1. 程式人生 > >從零開始學 Web 之 JS 高級(二)原型鏈,原型的繼承

從零開始學 Web 之 JS 高級(二)原型鏈,原型的繼承

console 多少 程序 cat hub inf 當前 構造函數 調用

大家好,這裏是「 從零開始學 Web 系列教程 」,並在下列地址同步更新......

  • github:https://github.com/Daotin/Web
  • 微信公眾號:Web前端之巔
  • 博客園:http://www.cnblogs.com/lvonve/
  • CSDN:https://blog.csdn.net/lvonve/

在這裏我會從 Web 前端零基礎開始,一步步學習 Web 相關的知識點,期間也會分享一些好玩的項目。現在就讓我們一起進入 Web 前端學習的冒險之旅吧!

技術分享圖片

一、原型鏈

原型鏈表示的是實例對象與原型對象之間的一種關系,這種關系是通過__proto__原型來聯系的。

1、原型的指向改變

實例對象的原型 __proto__

指向的是該對象的構造函數中的原型對象 prototype,如果該對象的構造函數的 prototype 指向改變了,那麽實例對象中的原型 __proto__ 的指向也會跟著改變。

例如:

Person.prototype = new Cat();

因為 Person.prototype = {}; 可以是一個對象,所以傳入另一個對象的實例函數是可以的,這時候 Person 的實例對象可以訪問 Cat 原型 prototype 中的屬性和方法,而不能再訪問自己 Person 中原型的屬性和方法了。

2、原型鏈的最終指向

實例對象的__proto__指向的是構造函數的原型對象 prototype,由於prototype也是個對象,所以也有 __proto__

,這個 __proto__ 指向的是 Object 的 prototype,而 Object 的 prototype 裏面的 __proto__ 指向的是 null。

示例:

function Person() {}
    var per = new Person();

    console.log(per.__proto__ === Person.prototype); // true
    console.log(Person.prototype.__proto__ === Object.prototype); // true
    console.log(Object.prototype.__proto__); // null

原型鏈圖示:

技術分享圖片

3、原型指向改變後添加原型方法

先看個案例:問下面程序有問題嗎?

function Person() {}
Person.prototype.eat = function () {};
function Student() {}
Student.prototype.say = function () {};
Student.prototype = new Person();

var stu = new  Student();
stu.say();

解答:stu.say(); 會報錯。因為 Student 的原型指向變成了 Person 的一個實例對象,Person 的實例對象鐘並沒有 say 方法,所以報錯。

解決辦法:在原型指向改變之後再添加原型方法。

function Person() {}
Person.prototype.eat = function () {};
function Student() {}

Student.prototype = new Person();
Student.prototype.say = function () {};

var stu = new  Student();
stu.say();

PS:這個時候就不會報錯, Student 添加的原型方法的位置是一個匿名 Person 的實例對象中,這裏是一個 Person 的實例對象,不是所有的,所以當你再 new 一個 Person 的實例對象的時候,不會有 say 方法。

4、實例對象和原型對象屬性重名問題

當實例對象訪問一個屬性的時候,會先從實例對象中找,找到了直接使用,找不到再到指向的原型對象中找,找到了使用,還是找不到,則為 undefined。

如何改變原型對象中的屬性的值呢?怎麽賦值的怎麽修改。

如果你使用 對象.屬性 = 值 的方式來賦值的話,如果這個屬性在實例對象中有的話,改變的是實例對象中屬性的值;如果實例對象中沒有這個屬性的話,則這次修改相當於給該實例對象添加了一個屬性,其指向的原型對象中相應的屬性的值並沒有被改變。


二、原型的繼承

1、原型的繼承

原型的第二個作用:繼承。目的也是節省內存空間。

通過改變子類原型的指向到父類的實例對象,可以實現繼承。

案例:

 // 父類:人
function Person(name, age) {
        this.name= name;
        this.age=age;
    }
    Person.prototype.eat = function () {
        console.log("eat()");
    };

    // 子類:學生
    function Student(sex) {
        this.sex=sex;
    }
    // 子類繼承父類只需要改變子類原型的指向到父類的實例對象。
    Student.prototype = new Person("Daotin", 18); 
    Student.prototype.study = function () {
        console.log("study()");
    };

    var stu = new Student("male");
    console.log(stu.name); // Daotin
    console.log(stu.age); // 18
    stu.eat(); // eat()

缺陷1:在改變子類原型對象的指向的時候,屬性在初始化的時候就固定了,那麽每個子類實例對象的值都固定了。

解決辦法:不需要子類原型的指向到父類的實例對象,只需要借用父類的構造函數。

    function Person(name, age) {
        this.name = name;
        this.age = age;
    }

    Person.prototype.eat = function () {
        console.log("eat()");
    };


    function Student(name, age, sex) {
        Person.call(this, name, age);
        this.sex = sex;
    }

    //Student.prototype = new Person("Daotin", 18);
    Student.prototype.study = function () {
        console.log("study()");
    };

    var stu = new Student("Daotin", 18, "male");
    console.log(stu.name);
    console.log(stu.age);
    console.log(stu.sex);
    stu.eat(); // 不能訪問

Person.call(this, name, age);第一個參數 this,表示當前對象,意思是當前對象呼叫 Person,將 name 和 age 傳過來,具體傳多少,我自己指定。這樣不同的子類,通過自己可以設置不同的屬性。

缺陷2stu.eat();不能訪問了,就是父類原型方法不能繼承了。

解決辦法組合繼承(原型方式繼承 + 借用構造函數繼承)

    function Person(name, age) {
        this.name = name;
        this.age = age;
    }

    Person.prototype.eat = function () {
        console.log("eat()");
    };

    function Student(name, age, sex) {
        Person.call(this, name, age); // 借用父類構造函數,實現父類屬性的繼承
        this.sex = sex;
    }

    Student.prototype = new Person(); // 不傳參數了,實現原型方法的繼承
    Student.prototype.study = function () {
        console.log("study()");
    };

    var stu = new Student("Daotin", 18, "male");
    console.log(stu.name);
    console.log(stu.age);
    console.log(stu.sex);
    stu.eat();
    stu.study();

Student.prototype = new Person();// 不傳參數了,實現原型方法的繼承。

Person.call(this, name, age);// 借用父類構造函數,實現父類屬性的繼承。

2、拷貝繼承

就是把對象中需要共享的屬性和方法直接以遍歷的方式復制到了另一個對象中。

    function Person(name, age) {
        this.name = name;
        this.age = age;
    }

    Person.prototype.eat = function () {
        console.log("eat()");
    };

    var per = {};

    // 循環拷貝
    for(var key in Person.prototype) {
        per[key] = Person.prototype[key];
    }
    console.log(per);

三、復習

1、函數的聲明和函數表達式的區別

// 函數的聲明
    if(true) {
        function f1() {
            console.log("f1()--1");
        }
    } else {
        function f1() {
            console.log("f1()--2");
        }
    }
    f1();

函數聲明如果放在 if-else- 裏面,chrome 和 firefox 輸出 f1()--1,IE8 下輸出 f1()--2,因為函數聲明會提前,第二個將第一個覆蓋了。

// 函數表達式
    var func;
    if(true) {
        func = function () {
            console.log("f1()--1");
        };
    } else {
        func = function () {
            console.log("f1()--2");
        };
    }
    func();

函數表達式的方式,輸出都是 f1()--1。所以盡量使用函數表達式。

2、嚴格模式

function func() {
  console.log(this) // window
}
func(); 

正常情況下是證正確的。

"use strict";
function func() {
  console.log(this) // window
}
window.func(); // 嚴格模式下必須加 window,因為他認為函數是一個方法,方法必須通過對象來調用的。

2.1、函數也是對象,對象不一定是函數(比如:Math)。

只要有 __proto__ 的就是對象;

只有要 prototype 的就是函數,因為函數才會調用 prototype 屬性。

對象不一定是函數:比如 Math,中有 __proto__ ,但是沒有 prototype

2.2、所有的函數都是由 Function 構造函數創建的實例對象。

既然函數是對象,那麽是什麽構造函數創建的呢?

var f1 = new Function("a", "b", "return a+b");
f1(1,2);

// 上面相當於:函數的聲明
function f1(a, b) {
  return a+b;
}
f1(1,2);

// 相當於:函數表達式
var f1 = function (a, b) {
  return a+b;
}
f1(1,2);

那麽 Function 是個什麽東西呢?

經查看是對象也是函數?然後查看它的 __proto__ 指向的是 Object的原型對象。所有的對象指向的都是Object的原型對象。

技術分享圖片

技術分享圖片

從零開始學 Web 之 JS 高級(二)原型鏈,原型的繼承