【進階5-1期】從新認識構造函數、原型和原型鏈

更新:謝謝你們的支持,最近折騰了一個博客官網出來,方便你們系統閱讀,後續會有更多內容和更多優化,猛戳這裏查看前端

------ 如下是正文 ------webpack

引言

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

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

下圖是本文的思惟導圖,高清思惟導圖和更多文章請看個人 Githubweb

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

對於基本類型來講是隻讀的,好比 一、「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() {
	// 一、建立一個空的對象
    var obj = new Object(),
	// 二、得到構造函數,同時刪除 arguments 中第一個參數
    Con = [].shift.call(arguments);
	// 三、連接到原型,obj 能夠訪問構造函數原型中的屬性
    Object.setPrototypeOf(obj, Con.prototype);
	// 四、綁定 this 實現繼承,obj 能夠訪問到構造函數中的屬性
    var ret = Con.apply(obj, arguments);
	// 五、優先返回構造函數返回的對象
	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() {
	// 一、得到構造函數,同時刪除 arguments 中第一個參數
    Con = [].shift.call(arguments);
	// 二、建立一個空的對象並連接到原型,obj 能夠訪問構造函數原型中的屬性
    var obj = Object.create(Con.prototype);
	// 三、綁定 this 實現繼承,obj 能夠訪問到構造函數中的屬性
    var ret = Con.apply(obj, arguments);
	// 四、優先返回構造函數返回的對象
	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…

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

相關文章
相關標籤/搜索