ES5 的構造函數原型鏈繼承

構造函數

構造函數,就是專門用來生成實例對象的函數。一個構造函數,可以生成多個實例對象,這些實例對象都有相同的結構。

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

爲了與普通函數區別,構造函數名字的第一個字母通常大寫。

構造函數的特點有兩個:

  • 函數體內部使用了 this 關鍵字,代表了所要生成的對象實例。
  • 生成對象的時候,必須使用 new 命令。

new 命令

基本用法

new 命令的作用,就是執行構造函數,返回一個實例對象。

let a = new Person('dora');
a.name      // dora

new 命令本身就可以執行構造函數,所以後面的構造函數可以帶括號,也可以不帶括號,但爲了表明是函數調用,推薦使用括號表示更明確的語義。

new 命令的原理

使用 new 命令時,它後面的函數依次執行下面的步驟:

  1. 創建一個空對象,作爲將要返回的對象實例。let o = new Object();
  2. 將這個空對象的原型,指向構造函數的prototype屬性。 Object.setPrototypeOf(o,Foo.prototype);
  3. 將構造函數的 this 綁定到新創建的空對象上。Foo.call(o);
  4. 始執行構造函數內部的代碼。

如果構造函數內部有 return 語句,而且後面跟着一個對象,則 new 命令會返回 return 語句指定的對象;否則,就會不管 return 語句,返回 this 對象,且不會執行 return 後面的語句。

function Person(name) {
  this.name = name;
  if (name == undefined) {
    return {};
  }else if(typeof name != 'string'){
    return '姓名有誤';
  }
  console.log(111);
}

new Person();        //  {}
new Person(123);     //  {name: 123}

new Person('dora');  
// {name:'dora'}
// 111

new.target

如果當前函數是 new 命令調用的,在函數內部的 new.target 屬性指向當前函數,否則爲 undefined

function F() {
  console.log(new.target === F);
}

F()       // false
new F()   // true

使用這個屬性,可以判斷函數調用時,是否使用了 new 命令。

function F() {
  if (!new.target) {
    throw new Error('請使用 new 命令調用!');
  }
}

F()       // false
new F()   // true

強制使用 new 命令

如果不使用 new 命令執行構造函數就會引發一些意想不到的結果,所以爲了保證構造函數必須與 new 命令一起使用,除了 new.target 之外也可以有以下兩個解決辦法。

1. 構造函數內部使用嚴格模式

在構造函數內部第一行加上 use strict。這樣的話,一旦忘了使用 new 命令,直接調用構造函數就會報錯。

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

Person()
//  TypeError: Cannot set property 'name' of undefined

報錯原因是因爲不加 new 調用構造函數時,this 指向全局對象,而嚴格模式下,this 不能指向全局對象,默認等於 undefined,給 undefined 添加屬性肯定會報錯。

2. 在構造函數內部通過 instanceof 判斷是否使用 new 命令

function Person(name, age) {
  if (!(this instanceof Person)) {
    return new Person(name, age);
  }

  this.name = name;
  this.age = age;
}

Person('dora', 18).name          // dora
(new Person('dora', 18)).age     // 18

在構造函數內部判斷 this 是否是構造函數的實例,如果不是,則直接返回一個實例對象。

prototype 原型

任何函數都有一個 prototype 屬性,這個屬性稱爲函數的“原型”,屬性值是一個對象。只有函數有原型屬性

function f(){}
typeof f.prototype   // "object"

對於普通函數來說,原型沒有什麼用。但對於構造函數來說,通過 new 生成實例的時候,該屬性會自動成爲實例對象的原型對象。

prototype 屬性的作用

用來定義所有實例對象共用的屬性和方法。

如果將對象的方法寫入構造函數中,則 new 多少個實例,方法將會被複制多少次,雖然複製出來的函數是一樣的,但分別指向不同的引用地址,不利於函數的複用。

function Person(){
  this.name = function(){
      console.log('dora');
  }
}
let p1 = new Person();
let p2 = new Person();

p1.name === p2.name     // false

因此,將所有的屬性都定義在構造函數裏,所有的方法都定義在構造函數的原型中。這樣,實例的方法都指向同一個引用地址,內存消耗小很多。

constructor 屬性

函數原型 prototype 對象的屬性,指向這個原型所在的構造函數,可以被所有實例對象繼承,指向構造自己的構造函數。

Person.prototype.constructor.name   //  "Person"
p1.constructor.name                 //  "Person"

函數的 name 屬性返回函數名。

給 prototype 添加屬性

1. 點語法追加屬性

Person.prototype.sayHi = function(){
    console.log('Hi,I am Dora');
}

2. 覆蓋原型對象

Person.prototype = {
  sayHi: function(){
      console.log('Hi,I am Dora');
  }
}

這樣用對象字面量直接覆蓋,會讓 constructor 與構造函數失聯,可以手動補上這個屬性。

Person.prototype = {
  constructor: Person,
  sayHi: function(){
      console.log('Hi,I am Dora');
  }
}

定義構造函數原型中的方法時儘量不要相互嵌套,各方法最好相互獨立。

_proto_ 原型對象

任何一個對象都有 __proto__ 屬性,這個屬性稱爲對象的“原型對象”,一個對象的原型對象就是它的構造函數的 prototype

function Person(){}
let p1 = new Person();

p1.__proto__ === Person.prototype  // true

Object.getPrototypeOf(obj)

__proto__ 並不是語言本身的屬性,這是各大瀏覽器廠商添加的私有屬性,雖然目前很多瀏覽器都可以識別這個屬性,但依舊不建議在生產環境下使用,避免對環境產生依賴。

生產環境下,我們可以使用 Object.getPrototypeOf(obj) 方法來獲取參數對象的原型。

Object.getPrototypeOf(p1) === Person.prototype   // true

原型鏈機制

當訪問對象的屬性時,如果這個對象沒有這個屬性,系統就會查找這個對象的 __proto__ 原型對象,原型對象也是個對象,也有自己的 __proto__ 原型對象,然後就會按照這個原型鏈依次往上查找,直到原型鏈的終點 Object.prototype

Object() 是系統內置的構造函數,用來創建對象的, Object.prototype 是所有對象的原型鏈頂端,而Object.prototype 的原型對象是 null

Object.getPrototypeOf(Object.prototype) // null

如果對象自身和它的原型都定義了一個同名屬性,那麼優先讀取對象自身的屬性。

繼承

繼承的核心是 子類構造函數的原型是父類構造函數的一個實例對象。

首先繼承父類的屬性

// 1. 父類構造函數
function Super(data){
  this.data = data;
};
Super.prototype.funName = function(){};

// 2. 子類構造函數
function Sub(){
  // 用來繼承父類的參數和屬性
  Super.apply(this, arguments);
}

其次繼承父類的方法

  1. 整體繼承

    Sub.prototype = Object.create(Super.prototype);
    
    // or
    
    Sub.prototype = new Super();
  2. 單個方法的繼承

    Sub.prototype.funName = function(){
      Super.prototype.funName.call(this);
      // some other code
    }

最後需要改變 constructor 指向

此時子類實例的 constructor 指向父類構造函數 Super,需手動改變。

Sub.prototype.constructor = Sub;

多重繼承

ES5 沒有多重繼承功能,即不允許一個對象同時繼承多個對象,但可通過變通方法實現這個功能。

function S(){
  M1.call(this);
  M2.call(this);
};
S.prototype = Object.create(M1.prototype); // 繼承 M1
Object.assign(S.prototype,M2.prototype);   // 繼承鏈上加入 M2
S.prototype.constructor = S;               // 指定構造函數。

實例驗證

instanceof 運算符

instanceof 運算符返回一個布爾值,表示對象是否爲某個構造函數的實例。繼承的子類實例也是父類的實例,因此繼承的也爲 true

let d = new Date();
d instanceof Date    // true
d instanceof Object  // true

Object.prototype.isPrototypeOf()

實例對象可繼承 isProtorypeOf()方法,用來判斷該對象是否爲參數對象的原型對象。

只要實例對象處在參數對象的原型鏈上,isPrototypeOf() 方法都返回 true

let o1 = {};
let o2 = Object.create(o1);
let o3 = Object.create(o2);

o2.isPrototypeOf(o3)   // true
o1.isPrototypeOf(o3)   // true
o2.isPrototypeOf(o2)   // false
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章