JS
系列暫定 27 篇,從基礎,到原型,到異步,到設計模式,到架構模式等,html
本篇是 JS
系列中最重要的一章,花費 3 分鐘便可理解,若是你已瞭解,快速瀏覽便可。前端
本篇文章主講構造函數、原型以及原型鏈,包括 Symbol
是否是構造函數、constructor
屬性是否只讀、prototype
、__proto__
、[[Prototype]]
、原型鏈。git
在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()
建立的。設計模式
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 仍然是基於原型的)。安全
let a = {}
實際上是 let a = new Object()
的語法糖let a = []
實際上是 let a = new Array()
的語法糖function Foo(){ ... }
實際上是 var Foo = new Function(...)
instanceof
判斷一個函數是否爲一個變量的構造函數Symbol 是基本數據類型,它並非構造函數,由於它不支持 new Symbol()
語法。咱們直接使用Symbol()
便可。markdown
let an = Symbol("An"); let an1 = new Symbol("An"); // Uncaught TypeError: Symbol is not a constructor 複製代碼
可是,Symbol()
能夠獲取到它的 constructor 屬性架構
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)
獲取全局惟一的 symbolSymbol.for('An') === Symbol.for("An"); // true 複製代碼
它從運行時的 symbol 註冊表中找到對應的 symbol,若是找到了,則返回它,不然,新建一個與該鍵關聯的 symbol,並放入全局 symbol 註冊表中。
// 實現可迭代協議,使迭代器可迭代: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 來實現拆箱操做(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 代替 [[class]] 屬性(ES5開始) let o = { [Symbol.toStringTag]: "MyObject" } console.log(o + ""); // [object MyObject] 複製代碼
對於引用類型來講 constructor
屬性值是能夠修改的,可是對於基本類型來講是隻讀的。
function An() { this.value = "An"; }; function Anran() {}; Anran.prototype.constructor = An; // 原型鏈繼承中,對 constructor 從新賦值 let anran = new Anran(); // 建立 Anran 的一個新實例 console.log(anran); 複製代碼
這說明,依賴一個引用對象的 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); 複製代碼
注意:null
和 undefined
是沒有 constructor
屬性的。
首先,貼上
圖片來自於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()// 原型的方法 複製代碼
全部函數都有一個 prototype
(顯式原型)屬性,屬性值也是一個普通的對象。對象以其原型爲模板,從原型繼承方法和屬性,這些屬性和方法定義在對象的構造器函數的 prototype
屬性上,而非對象實例自己。
但有一個例外: Function.prototype.bind()
,它並無 prototype 屬性
let fun = Function.prototype.bind(); // ƒ () { [native code] } 複製代碼
當咱們建立一個函數時,例如
function Foo () {} 複製代碼
prototype
屬性就被自動建立了
從上面這張圖能夠發現,Foo
對象有一個原型對象 Foo.prototype
,其上有兩個屬性,分別是 constructor
和 __proto__
,其中 __proto__
已被棄用。
構造函數 Foo
有一個指向原型的指針,原型 Foo.prototype
有一個指向構造函數的指針 Foo.prototype.constructor
,這就是一個循環引用,即:
Foo.prototype.constructor === Foo; // true 複製代碼
__proto__
每一個實例對象(object )都有一個隱式原型屬性(稱之爲 __proto__
)指向了建立該對象的構造函數的原型。也就時指向了函數的 prototype
屬性。
function Foo () {} let foo = new Foo() 複製代碼
當 new Foo()
時,__proto__
被自動建立。而且
foo.__proto__ === Foo.prototype; // true 複製代碼
即:
__proto__
發音 dunder proto,最早被 Firefox使用,後來在 ES6 被列爲 Javascript 的標準內建屬性。
[[Prototype]]
是對象的一個內部屬性,外部代碼沒法直接訪問。
遵循 ECMAScript 標準,someObject.[[Prototype]] 符號用於指向 someObject 的原型
__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__
。
新生成了一個對象
連接到原型
綁定 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 實現 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; }; 複製代碼
__proto__
屬性,屬性值是一個普通的對象,該原型對象也有一個本身的原型對象(__proto__
) ,層層向上直到一個對象的原型對象爲 null
。根據定義,null
沒有原型,並做爲這個原型鏈 中的最後一個環節。__proto__
(即它的構造函數的 prototype
)中尋找。每一個對象擁有一個原型對象,經過 __proto__
指針指向上一個原型 ,並從中繼承方法和屬性,同時原型對象也可能擁有原型,這樣一層一層,最終指向 null
,這種關係被稱爲原型鏈(prototype chain)。根據定義,null
沒有原型,並做爲這個原型鏈中的最後一個環節。
原型鏈的基本思想是利用原型,讓一個引用類型繼承另外一個引用類型的屬性及方法。
// 構造函數 function Foo(name) { this.name = name } // 建立實例 let f = new Foo('some') // 測試 f.toString() // f.__proto__.__proto__中尋找 複製代碼
f.__proto__=== Foo.prototype
,Foo.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') })// 鏈式操做 複製代碼
Symbol
是基本數據類型,並非構造函數,由於它不支持語法 new Symbol()
,但其原型上擁有 constructor
屬性,即 Symbol.prototype.constructor
。constructor
是能夠修改的,但對於基本類型來講它是隻讀的, null
和 undefined
沒有 constructor
屬性。__proto__
是每一個實例對象都有的屬性,prototype
是其構造函數的屬性,在實例上並不存在,因此這兩個並不同,但 foo.__proto__
和 Foo.prototype
指向同一個對象。__proto__
屬性在 ES6
時被標準化,但由於性能問題並不推薦使用,推薦使用 Object.getPrototypeOf()
。__proto__
指針指向上一個原型 ,並從中繼承方法和屬性,同時原型對象也可能擁有原型,這樣一層一層向上,最終指向 null
,這就是原型鏈。null
)暫時就這些,後續我將持續更新
想看更過系列文章,點擊前往 github 博客主頁
1. 若有任何問題或更獨特的看法,歡迎評論或直接聯繫瓶子君(公衆號回覆 123 便可)!
2. 歡迎關注:前端瓶子君,每日更新!