一、前言

介紹建構函式,原型,原型鏈。比如說經常會被問道:symbol是不是建構函式;constructor屬性是否只讀;prototype、[[Prototype]]和__proto__的區別;什麼是原型鏈?等等問題

 

二、建構函式

1、什麼建構函式

建構函式就是通過new關鍵詞生成例項的函式。

js的建構函式和其他語言不一樣,一般規範都是首字母大寫。

首先我們來看一下這個栗子:

// saucxs
function Parent(age) {
    this.age = age;
}
var p = new Parent(30);
console.log(p);  //見下圖
console.log(p.constructor);  // ƒ Parent(age){this.age = age;}
p.constructor === Parent; // true
p.constructor === Object; // false

這就是一個典型的建構函式,建構函式本身也是個函式,與普通區別不大,主要區別就是:建構函式使用new生成例項,直接呼叫就是普通函式。

 

2、constructor屬性

返回建立例項物件的Object建構函式的引用。此屬性的值對函式本身的引用,而不是一個包含函式名稱的字串。

所有物件都會從它的原型上繼承一個constructor屬性:

var o = {};
o.constructor === Object; // true

var o = new Object;
o.constructor === Object; // true

var a = [];
a.constructor === Array; // true

var a = new Array;
a.constructor === Array // true

var n = new Number(3);
n.constructor === Number; // true

那麼普通函式建立的例項有沒有constructor屬性呢?

// saucxs
// 普通函式
function parent2(age) {
    this.age = age;
}
var p2 = parent2(50);
console.log(p2);
// undefined

// 普通函式
function parent3(age) {
    return {
        age: age
    }
}
var p3 = parent3(50);
console.log(p3.constructor);  //ƒ Object() { [native code] }
p3.constructor === parent3;  // false
p3.constructor === Object; // true

上面程式碼說明:

(1)普通函式在內部有return操作的就有constructor屬性,沒有return的沒有constructor屬性;

(2)有constructor屬性的普通函式的constructor屬性值不是普通函式本身,是Object。

 

3、symbol是建構函式嗎?

MDN 是這樣介紹 `Symbol` 的

The `Symbol()` function returns a value of type **symbol**, has static properties that expose several members of built-in objects, has static methods that expose the global symbol registry, and resembles a built-in object class but is incomplete as a constructor because it does not support the syntax "`new Symbol()`".

Symbol是基本資料型別,作為建構函式它不完整,因為不支援語法new Symbol(),如果要生成例項直接使用Symbol()就可以的。

// saucxs
new Symbol(123); // Symbol is not a constructor

Symbol(123); // Symbol(123)

雖然Symbol是基本資料型別,但是Symbol(1234)例項可以獲取constructor屬性值。

// saucxs
var sym = Symbol(123); 
console.log( sym );  // Symbol(123)

console.log( sym.constructor ); // ƒ Symbol() { [native code] }
sym.constructor === Symbol;  //true
sym.constructor === Object;  //false

這裡的constructor屬性來自哪裡?其實是Symbol原型上的,預設為Symbol函式。

 

4、constructor的值是隻讀的嗎?

回答:如果是引用型別的constructor屬性值是可以修改的,如果是基本型別的就是隻讀的。

引用型別的情況,修改這個很好理解,比如原型鏈繼承的方案中,就是對constructor重新賦值的修正。

// saucxs
function Foo() {
    this.value = 42;
}
Foo.prototype = {
    method: function() {}
};

function Bar() {}

// 設定 Bar 的 prototype 屬性為 Foo 的例項物件
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World';

Bar.prototype.constructor === Object; 
//true
// 修正 Bar.prototype.constructor 為 Bar 本身
Bar.prototype.constructor = Bar;

var test = new Bar() // 建立 Bar 的一個新例項
console.log(test);

 

對於基本型別來說是隻讀的,比如:1, "saucxs", true, Symbol, null, undefined。null和undefined也是沒有constructor屬性的。

// saucxs
function Type() { };
var	types = [1, "muyiy", true, Symbol(123)];

for(var i = 0; i < types.length; i++) {
	types[i].constructor = Type;
	types[i] = [ types[i].constructor, types[i] instanceof Type, types[i].toString() ];
};

console.log( types.join("\n") );
// function Number() { [native code] },false,1
// function String() { [native code] },false,muyiy
// function Boolean() { [native code] },false,true
// function Symbol() { [native code] },false,Symbol(123)

為什麼會這樣?因為建立他們的是隻讀的原生建構函式(native constructors),這個栗子說明依賴一個物件的constructor屬性並不安全。

 

三、原型

3.1 prototype屬性

每一個物件都擁有一個原型物件,物件以其原型為模板,從原型整合方法和屬性,這些屬相和方法都在物件的構造器函式的prototype屬性上,而不是物件例項本身上。

上圖發現:

1、Parent物件有一個原型物件Parent.prototype,原型物件上有兩個屬性,分別為:constructor和__proto__,其中__proto__已被棄用。

2、建構函式Parent有一個指向原型的指標constructor;原型Parent.prototype有一個指向建構函式的指標Parent.prototype.constrcutor,其實就是一個迴圈引用。

 

3.2 __proto__屬性

上圖中可以看到Parent原型(Parent.prototype)上有一個__proto__屬性,這是一個訪問器屬性(即getter函式和setter函式)。作用:通過__proto__可以訪問到物件的內部[[Prototype]](一個物件或者null)

`__proto__` 發音 dunder proto,最先被 Firefox使用,後來在 ES6 被列為 Javascript 的標準內建屬性。

`[[Prototype]]` 是物件的一個內部屬性,外部程式碼無法直接訪問。

// saucxs
function Parent(){}; 
var p = new Parent();
console.log(p);
console.log(Parent.prototype);

1、p.__proto__獲取的是物件的原型,__proto__是每一個例項上都有的屬性;

2、prototype是建構函式的屬性;

3、p.__proto__和Parent.prototype指向同一個物件。

// saucxs
function Parent() {}
var p = new Parent();
p.__proto__ === Parent.prototype
// true

所以建構函式Parent,Parent.prototype和p之間的關係,如下圖所示:

 

注意1:`__proto__` 屬性在 `ES6` 時才被標準化

以確保 Web 瀏覽器的相容性,但是不推薦使用,除了標準化的原因之外還有效能問題。為了更好的支援,推薦使用 `Object.getPrototypeOf()`。

如果要讀取或修改物件的 `[[Prototype]]` 屬性,建議使用如下方案,但是此時設定物件的 `[[Prototype]]` 依舊是一個緩慢的操作,如果效能是一個問題,就要避免這種操作。

如果要建立一個新物件,同時繼承另一個物件的 `[[Prototype]]` ,推薦使用 `Object.create()`。

// saucxs
function Parent() {
    age: 50
};
var p = new Parent();
var child = Object.create(p);

這裡 `child` 是一個新的空物件,有一個指向物件 p 的指標 `__proto__`。

 

四、原型鏈

每一個物件擁有一個原型物件,通過__proto__指標指向上一個原型,並從中繼承方法和屬性,同時原型物件也可能擁有原型,這樣一層層的,最終指向null。這種關係成為原型鏈(prototype chain),作用:通過原型鏈一個物件會擁有定義在其他物件中的屬性和方法。

// saucxs
function Parent(age) {
    this.age = age;
}

var p = new Parent(50);
p.constructor === Parent; // true

p.constructor指向Parent,那麼是不是意味著p例項化存在constructor屬性呢?並不存在,列印一下p:

有圖可以知道,例項化物件p本身沒有constructor屬性,是通過原型鏈向上查詢__proto__,最終找到constructor屬性,該屬性指向Parent

// saucxs
function Parent(age) {
    this.age = age;
}
var p = new Parent(50);

p;	// Parent {age: 50}
p.__proto__ === Parent.prototype; // true
p.__proto__.__proto__ === Object.prototype; // true
p.__proto__.__proto__.__proto__ === null; // true

下圖展示原型鏈執行機制。

 

五、總結

1、Symbol是基本資料型別,作為建構函式並不完整,因為不支援語法new Symbol(),但是原型上擁有constructor屬性,即Symbol.prototype.constructor。

2、引用型別constructor屬性值是可以修改的,但是對於基本型別的是隻讀的,當然null和undefined沒有constructor屬性。

3、__proto__是每個例項上都有的屬性,prototype是建構函式的屬性,這兩個不一樣,但是p.__proto__和Parent.prototype是指向同一個物件。

4、__proto__屬性在ES6時被標準化,但是因為效能問題並不推薦使用,推薦使用Object.getPropertyOf()。

5、每個物件擁有一個原型物件,通過__ptoto_指標指向上一個原型,並從中繼承方法和屬性,同時原型物件也可能擁有原型,這樣一層已成的,最終指向null,這就是原型鏈。

 

六、參考

1、原型物件

2、Objcet.prototype.constructor

3、Object.prototype.__proto__

4、Symbol

5、原型

 

文章首發地址(sau交流學習社群)

&n