JS 系列二:深刻 constructor、prototype、__proto__、[[Prototype]] 及 原型鏈

引言

JS系列暫定 27 篇,從基礎,到原型,到異步,到設計模式,到架構模式等,html

本篇是 JS系列中最重要的一章,花費 3 分鐘便可理解,若是你已瞭解,快速瀏覽便可。前端

本篇文章主講構造函數、原型以及原型鏈,包括 Symbol 是否是構造函數、constructor 屬性是否只讀、prototype 、__proto__[[Prototype]]  、原型鏈。git

1、基礎入門

1. 對象

在JS中,萬物皆對象,對象又分爲普通對象和函數對象,其中 Object、Function 爲 JS 自帶的函數對象。github

let obj1 = {}; 
let obj2 = new Object();
let obj3 = new fun1()

function fun1(){}; 
let fun2 = function(){};
let fun3 = new Function('some','console.log(some)');

// JS自帶的函數對象
console.log(typeof Object); //function 
console.log(typeof Function); //function 

// 普通對象
console.log(typeof obj1); //object 
console.log(typeof obj2); //object 
console.log(typeof obj3); //object

// 函數對象
console.log(typeof fun1); //function 
console.log(typeof fun2); //function 
console.log(typeof fun3); //function 
複製代碼

凡是經過 new Function() 建立的對象都是函數對象,其餘的都是普通對象,Function Object 是經過 New Function() 建立的。設計模式

2. 構造函數

function Foo(name, age) {
    // this 指向 Foo
    this.name = name
    this.age = age
    this.class = 'class'
    // return this // 默認有這一行
}

// Foo 的實例
let f = new Foo('aa', 20)
複製代碼

每一個實例都有一個 constructor(構造函數)屬性,該屬性指向對象自己。數組

f.constructor === Foo // true
複製代碼

構造函數自己就是一個函數,與普通函數沒有任何區別,不過爲了規範通常將其首字母大寫。構造函數和普通函數的區別在於,使用 new 生成實例的函數就是構造函數,直接調用的就是普通函數。瀏覽器

JS 自己不提供一個 class 實現。(在 ES2015/ES6 中引入了 class 關鍵字,但只是語法糖,JavaScript 仍然是基於原型的)。安全

3. 構造函數擴展

  • let a = {} 實際上是 let a = new Object() 的語法糖
  • let a = [] 實際上是 let a = new Array() 的語法糖
  • function Foo(){ ... } 實際上是 var Foo = new Function(...)
  • 可使用 instanceof 判斷一個函數是否爲一個變量的構造函數

4. Symbol 是構造函數嗎?

Symbol 是基本數據類型,它並非構造函數,由於它不支持 new Symbol() 語法。咱們直接使用Symbol() 便可。架構

let an = Symbol("An");

let an1 = new Symbol("An"); 
// Uncaught TypeError: Symbol is not a constructor
複製代碼

可是,Symbol() 能夠獲取到它的 constructor 屬性app

Symbol("An").constructor; 
// ƒ Symbol() { [native code] }
複製代碼

這個 constructor 其實是 Symbol 原型上的,即

Symbol.prototype.constructor; 
// ƒ Symbol() { [native code] }
複製代碼

對於 Symbol,你還須要瞭解如下知識點:

Symbol() 返回的 symbol 值是惟一
Symbol("An") === Symbol("An"); 
// false
複製代碼
能夠經過 Symbol.for(key) 獲取全局惟一的 symbol
Symbol.for('An') === Symbol.for("An"); // true
複製代碼

它從運行時的 symbol 註冊表中找到對應的 symbol,若是找到了,則返回它,不然,新建一個與該鍵關聯的 symbol,並放入全局 symbol 註冊表中。

Symbol.iterator :返回一個對象的迭代器
// 實現可迭代協議,使迭代器可迭代:Symbol.iterator
function createIterator(items) {
    let i = 0
    return {
        next: function () {
            let done = (i >= items.length)
            let value = !done ? items[i++] : undefined
            return {
                done: done,
                value: value
            }
        },
        [Symbol.iterator]: function () {
        	return this
    	}
    }
}
const iterator = createIterator([1, 2, 3]);
[...iterator];		// [1, 2, 3]
複製代碼
Symbol.toPrimitive:將對象轉換成基本數據類型
// Symbol.toPrimitive 來實現拆箱操做(ES6 以後)
let obj = {
    valueOf: () => {console.log("valueOf"); return {}},
    toString: () => {console.log("toString"); return {}}
}
obj[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}
console.log(obj + "") 
// toPrimitive
// hello
複製代碼
Symbol.toStringTag:用於設置對象的默認描述字符串值
// Symbol.toStringTag 代替 [[class]] 屬性(ES5開始)
let o = { [Symbol.toStringTag]: "MyObject" }

console.log(o + ""); 
// [object MyObject]
複製代碼

5. constructor 的值是隻讀的嗎?

對於引用類型來講 constructor 屬性值是能夠修改的,可是對於基本類型來講是隻讀的。

引用類型
function An() {
    this.value = "An";
};
function Anran() {};

Anran.prototype.constructor = An; 
// 原型鏈繼承中,對 constructor 從新賦值

let anran = new Anran(); 
// 建立 Anran 的一個新實例

console.log(anran);
複製代碼
constructor

這說明,依賴一個引用對象的 constructor 屬性,並非安全的。

基本類型
function An() {};
let an = 1;
an.constructor = An;
console.log(an.constructor); 
// ƒ Number() { [native code] }
複製代碼

這是由於:原生構造函數(native constructors)是隻讀的

JS 對於不可寫的屬性值的修改靜默失敗(silently failed),但只會在嚴格模式下才會提示錯誤。

'use strict';
function An() {};
let an = 1;
an.constructor = An;
console.log(an.constructor); 
複製代碼
use strict

注意:null 和 undefined 是沒有 constructor 屬性的。

2、原型

首先,貼上

原型

圖片來自於http://www.mollypages.org/tutorials/js.mp,請根據下文仔細理解這張圖

在JS中,每一個對象都有本身的原型。當咱們訪問對象的屬性和方法時,JS 會先訪問對象自己的方法和屬性。若是對象自己不包含這些屬性和方法,則訪問對象對應的原型。

// 構造函數
function Foo(name) {
    this.name = name
}
Foo.prototype.alertName = function() {
    alert(this.name)
}
// 建立實例
let f = new Foo('some')
f.printName = function () {
    console.log(this.name)
}
// 測試
f.printName()// 對象的方法
f.alertName()// 原型的方法
複製代碼

1. prototype

全部函數都有一個 prototype (顯式原型)屬性,屬性值也是一個普通的對象。對象以其原型爲模板,從原型繼承方法和屬性,這些屬性和方法定義在對象的構造器函數的 prototype 屬性上,而非對象實例自己。

但有一個例外: Function.prototype.bind(),它並無 prototype 屬性

let fun = Function.prototype.bind(); 
// ƒ () { [native code] }
複製代碼

當咱們建立一個函數時,例如

function Foo () {}
複製代碼
FOO

prototype 屬性就被自動建立了

從上面這張圖能夠發現,Foo 對象有一個原型對象 Foo.prototype,其上有兩個屬性,分別是 constructor__proto__,其中 __proto__ 已被棄用。

構造函數 Foo 有一個指向原型的指針,原型 Foo.prototype 有一個指向構造函數的指針 Foo.prototype.constructor,這就是一個循環引用,即:

Foo.prototype.constructor === Foo; // true
複製代碼
constructor與prototype

2. __proto__

每一個實例對象(object )都有一個隱式原型屬性(稱之爲 __proto__ )指向了建立該對象的構造函數的原型。也就時指向了函數的 prototype 屬性。

function Foo () {}
let foo = new Foo()
複製代碼
Foo1

new Foo() 時,__proto__ 被自動建立。而且

foo.__proto__ === Foo.prototype; // true
複製代碼

即:

屏幕快照 2019-08-25 下午9 38 36

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

3. [[Prototype]]

[[Prototype]] 是對象的一個內部屬性,外部代碼沒法直接訪問。

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

4. 注意

__proto__ 屬性在 ES6 時才被標準化,以確保 Web 瀏覽器的兼容性,可是不推薦使用,除了標準化的緣由以外還有性能問題。爲了更好的支持,推薦使用 Object.getPrototypeOf()

經過現代瀏覽器的操做屬性的便利性,能夠改變一個對象的 [[Prototype]] 屬性, 這種行爲在每個JavaScript引擎和瀏覽器中都是一個很是慢且影響性能的操做,使用這種方式來改變和繼承屬性是對性能影響很是嚴重的,而且性能消耗的時間也不是簡單的花費在 obj.__proto__ = ... 語句上, 它還會影響到全部繼承來自該 [[Prototype]] 的對象,若是你關心性能,你就不該該在一個對象中修改它的 [[Prototype]]。相反, 建立一個新的且能夠繼承 [[Prototype]] 的對象,推薦使用 Object.create()

若是要讀取或修改對象的 [[Prototype]] 屬性,建議使用以下方案,可是此時設置對象的 [[Prototype]] 依舊是一個緩慢的操做,若是性能是一個問題,就要避免這種操做。

// 獲取(二者一致)
Object.getPrototypeOf()
Reflect.getPrototypeOf()

// 修改(二者一致)
Object.setPrototypeOf()
Reflect.setPrototypeOf()
複製代碼

若是要建立一個新對象,同時繼承另外一個對象的 [[Prototype]] ,推薦使用 Object.create()

function An() {};
var an = new An();
var anran = Object.create(an);
複製代碼

這裏 anran 是一個新的空對象,有一個指向對象 an 的指針 __proto__

5. new 的實現過程

  • 新生成了一個對象

  • 連接到原型

  • 綁定 this

  • 返回新對象

function new_object() {
  // 建立一個空的對象
  let obj = new Object()
  // 得到構造函數
  let Con = [].shift.call(arguments)
  // 連接到原型 (不推薦使用)
  obj.__proto__ = Con.prototype
  // 綁定 this,執行構造函數
  let result = Con.apply(obj, arguments)
  // 確保 new 出來的是個對象
  return typeof result === 'object' ? result : obj
}
複製代碼
優化 new 實現
// 優化後 new 實現
function create() {
  // 一、得到構造函數,同時刪除 arguments 中第一個參數
  Con = [].shift.call(arguments);
  // 二、建立一個空的對象並連接到原型,obj 能夠訪問構造函數原型中的屬性
  let obj = Object.create(Con.prototype);
  // 三、綁定 this 實現繼承,obj 能夠訪問到構造函數中的屬性
  let ret = Con.apply(obj, arguments);
  // 四、優先返回構造函數返回的對象
  return ret instanceof Object ? ret : obj;
};
複製代碼

6. 總結

  • 全部的引用類型(數組、對象、函數)都有對象特性,便可自由擴展屬性(null除外)。
  • 全部的引用類型,都有一個 __proto__ 屬性,屬性值是一個普通的對象,該原型對象也有一個本身的原型對象(__proto__) ,層層向上直到一個對象的原型對象爲 null。根據定義,null 沒有原型,並做爲這個原型鏈 中的最後一個環節。
  • 當試圖獲得一個對象的某個屬性時,若是這個對象自己沒有這個屬性,那麼會去它的 __proto__ (即它的構造函數的 prototype )中尋找。

3、原型鏈

每一個對象擁有一個原型對象,經過 __proto__ 指針指向上一個原型 ,並從中繼承方法和屬性,同時原型對象也可能擁有原型,這樣一層一層,最終指向 null,這種關係被稱爲原型鏈(prototype chain)。根據定義,null 沒有原型,並做爲這個原型鏈中的最後一個環節。

原型鏈的基本思想是利用原型,讓一個引用類型繼承另外一個引用類型的屬性及方法。

// 構造函數
function Foo(name) {
    this.name = name
}
// 建立實例
let f = new Foo('some')
// 測試
f.toString() 
// f.__proto__.__proto__中尋找
複製代碼

f.__proto__=== Foo.prototypeFoo.prototype 也是一個對象,也有本身的__proto__ 指向 Object.prototype, 找到toString()方法。

也就是

Function.__proto__.__proto__ === Object.prototype
複製代碼

原型鏈

下面是原型鏈繼承的例子

function Elem(id) {
    this.elem = document.getElementById(id)
}

Elem.prototype.html = function(val) {
    let elem = this.elem
    if (val) {
        elem.innerHtml = val
        return this // 鏈式操做
    } else {
        return elem.innerHtml
    }
}

Elem.prototype.on = function( type, fn) {
    let elem = this.elem
    elem.addEventListener(type, fn)
}

let div1 = new Elem('div1')
// console.log(div1.html())
div1.html('<p>hello</p>').on('click', function() {
    alert('clicked')
})// 鏈式操做
複製代碼

4、總結

  • Symbol 是基本數據類型,並非構造函數,由於它不支持語法 new Symbol(),但其原型上擁有 constructor 屬性,即 Symbol.prototype.constructor
  • 引用類型 constructor 是能夠修改的,但對於基本類型來講它是隻讀的, nullundefined 沒有 constructor 屬性。
  • __proto__ 是每一個實例對象都有的屬性,prototype 是其構造函數的屬性,在實例上並不存在,因此這兩個並不同,但 foo.__proto__Foo.prototype 指向同一個對象。
  • __proto__ 屬性在 ES6 時被標準化,但由於性能問題並不推薦使用,推薦使用 Object.getPrototypeOf()
  • 每一個對象擁有一個原型對象,經過 __proto__ 指針指向上一個原型 ,並從中繼承方法和屬性,同時原型對象也可能擁有原型,這樣一層一層向上,最終指向 null,這就是原型鏈。
  • 當試圖獲得一個對象的某個屬性時,若是這個對象自己沒有這個屬性,那麼會去它的原型中尋找,以及該對象的原型的原型,一層一層向上查找,直到找到一個名字匹配的屬性 / 方法或到達原型鏈的末尾(null

5、參考

暫時就這些,後續我將持續更新

系列文章

想看更過系列文章,點擊前往 github 博客主頁

走在最後

1. 若有任何問題或更獨特的看法,歡迎評論或直接聯繫瓶子君(公衆號回覆 123 便可)!

2. 歡迎關注:前端瓶子君,每日更新!

前端瓶子君
相關文章
相關標籤/搜索