【進階5-1期】重新認識建構函式、原型和原型鏈

木易楊說發表於2019-02-19

引言

前端進階系列已經到第 5 期啦,本期正式開始原型 Prototype 系列。

本篇文章重點介紹建構函式、原型和原型鏈相關知識,如果你還不知道 Symbol 是不是建構函式、constructor 屬性是否只讀、prototype[[Prototype]]__proto__ 的區別、什麼是原型鏈,建議你好好閱讀本文,希望對你有所幫助。

下圖是本文的思維導圖,高清思維導圖和更多文章請看我的 Github

1111

建構函式

什麼是建構函式

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

// 木易楊
function Parent(age) {
    this.age = age;
}

var p = new Parent(50);
p.constructor === Parent; // true
p.constructor === Object; // false
複製程式碼

建構函式本身就是一個函式,與普通函式沒有任何區別,不過為了規範一般將其首字母大寫。建構函式和普通函式的區別在於,使用 new 生成例項的函式就是建構函式,直接呼叫的就是普通函式。

那是不是意味著普通函式建立的例項沒有 constructor 屬性呢?不一定。

// 木易楊
// 普通函式
function parent2(age) {
    this.age = age;
}
var p2 = parent2(50);
// undefined

// 普通函式
function parent3(age) {
    return {
        age: age
    }
}
var p3 = parent3(50);
p3.constructor === Object; // true
複製程式碼

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(),Chrome 認為其不是建構函式,如果要生成例項直接使用 Symbol() 即可。(來自 MDN

// 木易楊
new Symbol(123); // Symbol is not a constructor 

Symbol(123); // Symbol(123)
複製程式碼

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

// 木易楊
var sym = Symbol(123); 
console.log( sym );
// Symbol(123)

console.log( sym.constructor );
// ƒ Symbol() { [native code] }
複製程式碼

這裡的 constructor 屬性來自哪裡?其實是 Symbol 原型上的,即 Symbol.prototype.constructor 返回建立例項原型的函式, 預設為 Symbol 函式。

constructor 值只讀嗎

這個得分情況,對於引用型別來說 constructor 屬性值是可以修改的,但是對於基本型別來說是隻讀的。

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

// 木易楊
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);
複製程式碼

image-20190210152306297

對於基本型別來說是隻讀的,比如 1、“muyiy”、true、Symbol,當然 nullundefined 是沒有 constructor 屬性的。

// 木易楊
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 屬性並不安全。

模擬實現 new

說到這裡就要聊聊 new 的實現了,實現程式碼如下。

// 木易楊
function create() {
	// 1、建立一個空的物件
    var obj = new Object(),
	// 2、獲得建構函式,同時刪除 arguments 中第一個引數
    Con = [].shift.call(arguments);
	// 3、連結到原型,obj 可以訪問建構函式原型中的屬性
    Object.setPrototypeOf(obj, Con.prototype);
	// 4、繫結 this 實現繼承,obj 可以訪問到建構函式中的屬性
    var ret = Con.apply(obj, arguments);
	// 5、優先返回建構函式返回的物件
	return ret instanceof Object ? ret : obj;
};
複製程式碼

之前寫過一篇文章解析 new 的模擬實現過程,如果你對實現過程還不瞭解的話點選閱讀。「【進階3-5期】深度解析 new 原理及模擬實現

原型

prototype

JavaScript 是一種基於原型的語言 (prototype-based language),這個和 Java 等基於類的語言不一樣。

每個物件擁有一個原型物件,物件以其原型為模板,從原型繼承方法和屬性,這些屬性和方法定義在物件的構造器函式的 prototype 屬性上,而非物件例項本身。

image-20190210223838636

從上面這張圖可以發現,Parent 物件有一個原型物件 Parent.prototype,其上有兩個屬性,分別是 constructor__proto__,其中 __proto__ 已被棄用。

建構函式 Parent 有一個指向原型的指標,原型 Parent.prototype 有一個指向建構函式的指標 Parent.prototype.constructor,如上圖所示,其實就是一個迴圈引用。

image-20190211154751602

__proto__

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

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

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

遵循 ECMAScript 標準,someObject.[[Prototype]] 符號用於指向 someObject 的原型。

image-20190211194108633

這裡用 p.__proto__ 獲取物件的原型,__proto__ 是每個例項上都有的屬性,prototype 是建構函式的屬性,這兩個並不一樣,但 p.__proto__Parent.prototype 指向同一個物件。

// 木易楊
function Parent() {}
var p = new Parent();
p.__proto__ === Parent.prototype
// true
複製程式碼

所以建構函式 ParentParent.prototypep 的關係如下圖。

image-20190211200314401

注意點

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

通過改變一個物件的 [[Prototype]] 屬性來改變和繼承屬性會對效能造成非常嚴重的影響,並且效能消耗的時間也不是簡單的花費在 obj.__proto__ = ... 語句上, 它還會影響到所有繼承自該 [[Prototype]] 的物件,如果你關心效能,你就不應該修改一個物件的 [[Prototype]]

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

// 木易楊
// 獲取
Object.getPrototypeOf()
Reflect.getPrototypeOf()

// 修改
Object.setPrototypeOf()
Reflect.setPrototypeOf()

複製程式碼

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

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

複製程式碼

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

優化實現 new

正如上面介紹的不建議使用 __proto__,所以我們使用 Object.create() 來模擬實現,優化後的程式碼如下。

// 木易楊
function create() {
	// 1、獲得建構函式,同時刪除 arguments 中第一個引數
    Con = [].shift.call(arguments);
	// 2、建立一個空的物件並連結到原型,obj 可以訪問建構函式原型中的屬性
    var obj = Object.create(Con.prototype);
	// 3、繫結 this 實現繼承,obj 可以訪問到建構函式中的屬性
    var ret = Con.apply(obj, arguments);
	// 4、優先返回建構函式返回的物件
	return ret instanceof Object ? ret : obj;
};
複製程式碼

原型鏈

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

我們看下面一個例子

// 木易楊
function Parent(age) {
    this.age = age;
}

var p = new Parent(50);
p.constructor === Parent; // true
複製程式碼

這裡 p.constructor 指向 Parent,那是不是意味著 p 例項存在 constructor 屬性呢?並不是。

我們列印下 p 值就知道了。

image-20190219214338236

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

// 木易楊
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
複製程式碼

下圖展示了原型鏈的運作機制。

image-20190213164902615

小結

  • Symbol 作為建構函式來說並不完整,因為不支援語法 new Symbol(),但其原型上擁有 constructor 屬性,即 Symbol.prototype.constructor
  • 引用型別 constructor 屬性值是可以修改的,但是對於基本型別來說是隻讀的,當然 nullundefined 沒有 constructor 屬性。
  • __proto__ 是每個例項上都有的屬性,prototype 是建構函式的屬性,這兩個並不一樣,但 p.__proto__Parent.prototype 指向同一個物件。
  • __proto__ 屬性在 ES6 時被標準化,但因為效能問題並不推薦使用,推薦使用 Object.getPrototypeOf()
  • 每個物件擁有一個原型物件,通過 __proto__ 指標指向上一個原型 ,並從中繼承方法和屬性,同時原型物件也可能擁有原型,這樣一層一層,最終指向 null,這就是原型鏈。

參考

進階系列目錄

  • 【進階1期】 呼叫堆疊
  • 【進階2期】 作用域閉包
  • 【進階3期】 this全面解析
  • 【進階4期】 深淺拷貝原理
  • 【進階5期】 原型Prototype
  • 【進階6期】 高階函式
  • 【進階7期】 事件機制
  • 【進階8期】 Event Loop原理
  • 【進階9期】 Promise原理
  • 【進階10期】Async/Await原理
  • 【進階11期】防抖/節流原理
  • 【進階12期】模組化詳解
  • 【進階13期】ES6重難點
  • 【進階14期】計算機網路概述
  • 【進階15期】瀏覽器渲染原理
  • 【進階16期】webpack配置
  • 【進階17期】webpack原理
  • 【進階18期】前端監控
  • 【進階19期】跨域和安全
  • 【進階20期】效能優化
  • 【進階21期】VirtualDom原理
  • 【進階22期】Diff演算法
  • 【進階23期】MVVM雙向繫結
  • 【進階24期】Vuex原理
  • 【進階25期】Redux原理
  • 【進階26期】路由原理
  • 【進階27期】VueRouter原始碼解析
  • 【進階28期】ReactRouter原始碼解析

交流

進階系列文章彙總如下,內有優質前端資料,覺得不錯點個star。

github.com/yygmind/blo…

我是木易楊,網易高階前端工程師,跟著我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高階前端的世界,在進階的路上,共勉!

【進階5-1期】重新認識建構函式、原型和原型鏈

相關文章