JavaScript
中沒有類的概念的,主要經過原型鏈來實現繼承。一般狀況下,繼承意味着複製操做,然而 JavaScript
默認並不會複製對象的屬性,相反,JavaScript
只是在兩個對象之間建立一個關聯(原型對象指針),這樣,一個對象就能夠經過委託訪問另外一個對象的屬性和函數,因此與其叫繼承,委託的說法反而更準確些。javascript
當咱們
new
了一個新的對象實例,明明什麼都沒有作,就直接能夠訪問toString
、valueOf
等原生方法。那麼這些方法是從哪裏來的呢?答案就是原型。前端
在控制檯打印一個空對象時,咱們能夠看到,有不少方法,已經「初始化」掛載在內置的 __proto__
對象上了。這個內置的 __proto__
是一個指向原型對象的指針,它會在建立一個新的引用類型對象時(顯示或者隱式)自動建立,並掛載到新實例上。當咱們嘗試訪問實例對象上的某一屬性 / 方法時,若是實例對象上有該屬性 / 方法時,就返回實例屬性 / 方法,若是沒有,就去 __proto__
指向的原型對象上查找對應的屬性 / 方法。這就是爲何咱們嘗試訪問空對象的 toString
和 valueOf
等方法依舊能訪問到的緣由,JavaScript
正式以這種方式爲基礎來實現繼承的。java
若是說實例的 __proto__
只是一個指向原型對象的指針,那就說明在此以前原型對象就已經建立了,那麼原型對象是何時被建立的呢?這就要引入構造函數的概念。webpack
其實構造函數也就只是一個普通的函數而已,若是這個函數可使用 new
關鍵字來建立它的實例對象,那麼咱們就把這種函數稱爲 構造函數。git
// 普通函數 function person () {} // 構造函數,函數首字母一般大寫 function Person () {} const person = new Person(); 複製代碼
原型對象正是在構造函數被聲明時一同建立的。構造函數被申明時,原型對象也一同完成建立,而後掛載到構造函數的 prototype
屬性上:程序員
原型對象被建立時,會自動生成一個 constructor
屬性,指向建立它的構造函數。這樣它倆的關係就被緊密地關聯起來了。github
細心的話,你可能會發現,原型對象也有本身的
__proto__
,這也不奇怪,畢竟萬物皆對象嘛。原型對象的 __proto__ 指向的是Object.prototype
。那麼Object.prototype.__proto__
存不存在呢?實際上是不存在的,打印的話會發現是null
。這也證實了Object
是JavaScript
中數據類型的起源。web
分析到這裏,咱們大概瞭解原型及構造函數的大概關係了,咱們能夠用一張圖來表示這個關係:面試
說完了原型,就能夠來講說原型鏈了,若是理解了原型機制,原型鏈就很好解釋了。其實上面一張圖上,那條被 __proto__
連接起來的鏈式關係,就稱爲原型鏈。express
原型鏈的做用:原型鏈如此的重要的緣由就在於它決定了 JavaScript
中繼承的實現方式。當咱們訪問一個屬性時,查找機制以下:
__proto__
去它的原型對象查找。Object.prototype
,若是找到目標屬性即返回,找不到就返回 undefined
,不會再往下找,由於在往下找 __proto__
就是 null
了。經過上面的解釋,對於構造函數生成的實例,咱們應該能瞭解它的原型對象了。JavaScript 中萬物皆對象,那麼構造函數確定也是個對象,是對象就有 __proto__
,那麼構造函數的 __proto__
是什麼?
咱們能夠打印出來看一下:
如今纔想起來全部的函數可使用 new Function()
的方式建立,那麼這個答案也就很天然了,有點意思,再來試試別的構造函數。
這也證實了,全部函數都是 Function
的實例。等一下,好像有哪裏不對,那麼 Function.__proto__
豈不是。。。
按照上面的邏輯,這樣說的話,Function
豈不是本身生成了本身?其實,咱們大可沒必要這樣理解,由於做爲一個 JS 內置對象,Function
對象在你腳本文件都還沒生成的時候就已經存在了,哪裏能本身調用本身,這個東西就相似於玄學中的「道」和「乾坤」,你能說明它們是誰生成的嗎,天地同壽日月同庚不生不滅。。。算了,在往下扯就要寫成修仙了=。=
至於爲何 Function.__proto__
等於 Function.prototype
有這麼幾種說法:
Function
的實例。call
bind
這些內置 API 的,這麼寫能夠很好的保證函數實例可以使用這些 API。關於原型、原型鏈和構造函數有幾點須要注意:
__proto__
是非標準屬性,若是要訪問一個對象的原型,建議使用 ES6 新增的 Reflect.getPrototypeOf
或者 Object.getPrototypeOf()
方法,而不是直接 obj.__proto__
,由於非標準屬性意味着將來可能直接會修改或者移除該屬性。同理,當改變一個對象的原型時,最好也使用 ES6
提供的 Reflect.setPrototypeOf
或 Object.setPrototypeOf
。let target = {}; let newProto = {}; Reflect.getPrototypeOf(target) === newProto; // false Reflect.setPrototypeOf(target, newProto); Reflect.getPrototypeOf(target) === newProto; // true 複製代碼
prototype
,除了 Function.prototype.bind()
以外。__proto__
,除了 Object.prototype
以外(其實它也是有的,之不過是 null
)。__proto__
都等於 Function.prototype
。Function.prototype
等於 Function.__proto__
。原型污染是指:攻擊者經過某種手段修改 JavaScript 對象的原型。
什麼意思呢,原理其實很簡單。若是咱們把 Object.prototype.toString
改爲這樣:
Object.prototype.toString = function () {alert('原型污染')}; let obj = {}; obj.toString(); 複製代碼
那麼當咱們運行這段代碼的時候瀏覽器就會彈出一個 alert
,對象原生的 toString
方法被改寫了,全部對象當調用 toString
時都會受到影響。
你可能會說,怎麼可能有人傻到在源碼裏寫這種代碼,這不是搬起石頭砸本身的腳麼?沒錯,沒人會在源碼裏這麼寫,可是攻擊者可能會經過表單或者修改請求內容等方式使用原型污染髮起攻擊,來看下面一種狀況:
'use strict'; const express = require('express'); const bodyParser = require('body-parser') const cookieParser = require('cookie-parser'); const path = require('path'); const isObject = obj => obj && obj.constructor && obj.constructor === Object; function merge(a, b) { for (var attr in b) { if (isObject(a[attr]) && isObject(b[attr])) { merge(a[attr], b[attr]); } else { a[attr] = b[attr]; } } return a } function clone(a) { return merge({}, a); } // Constants const PORT = 8080; const HOST = '0.0.0.0'; const admin = {}; // App const app = express(); app.use(bodyParser.json()) app.use(cookieParser()); app.use('/', express.static(path.join(__dirname, 'views'))); app.post('/signup', (req, res) => { var body = JSON.parse(JSON.stringify(req.body)); var copybody = clone(body) if (copybody.name) { res.cookie('name', copybody.name).json({ "done": "cookie set" }); } else { res.json({ "error": "cookie not set" }) } }); app.get('/getFlag', (req, res) => { var аdmin = JSON.parse(JSON.stringify(req.cookies)) if (admin.аdmin == 1) { res.send("hackim19{}"); } else { res.send("You are not authorized"); } }); app.listen(PORT, HOST); console.log(`Running on http://${HOST}:${PORT}`); 複製代碼
若是服務器中有上述的代碼片斷,攻擊者只要將 cookie
設置成{__proto__: {admin: 1}}
就能完成系統的侵入。
在看原型污染的解決方案以前,咱們能夠看下 lodash
團隊以前解決原型污染問題的手法:
代碼很簡單,只要是碰到有 constructor
或者 __proto__
這樣的敏感詞彙,就直接退出執行了。這固然是一種防止原型污染的有效手段,固然咱們還有其餘手段:
Object.create(null)
, 方法建立一個原型爲 null
的新對象,這樣不管對 原型作怎樣的擴展都不會生效:const obj = Object.create(null); obj.__proto__ = { hack: '污染原型的屬性' }; console.log(obj); // => {} console.log(obj.hack); // => undefined 複製代碼
使用 Object.freeze(obj)
凍結指定對象,使之不能被修改屬性,成爲不可擴展對象:
Object.freeze(Object.prototype); Object.prototype.toString = 'evil'; console.log(Object.prototype.toString); // => ƒ toString() { [native code] } 複製代碼
創建 JSON schema
,在解析用戶輸入內容時,經過 JSON schema
過濾敏感鍵名。
規避不安全的遞歸性合併。這一點相似 lodash
修復手段,完善了合併操做的安全性,對敏感鍵名跳過處理。
終於能夠來講說繼承了,先來看看繼承的概念,看下百度上是怎麼說的:
繼承是面向對象軟件技術當中的一個概念,與多態、封裝共爲面向對象的三個基本特徵。繼承可使得子類具備父類的屬性和方法或者從新定義、追加屬性和方法等。
這段對於程序員來講,這個解釋仍是比較好理解的。接着往下翻,我看到了一條重要的描述:
子類的建立能夠增長新數據、新功能,能夠繼承父類所有的功能,可是不能選擇性的繼承父類的部分功能。繼承是類與類之間的關係,不是對象與對象之間的關係。
這就尷尬了,JavaScript
裏哪裏來的類,只有對象。那照這麼說豈不是不能實現純正的繼承了?因此纔會有開頭那句話:與其叫繼承,委託的說法反而更準確些。
可是 JavaScript
是很是靈活的, 靈活這一特色給它帶來不少缺陷的同時,也締造出不少驚豔的優勢。沒有原生提供類的繼承沒關係,咱們能夠用更多元的方式來實現 JavaScript
中的繼承,好比說利用 Object.assign
:
let person = { name: null, age: null }; let man = Object.assign({}, person, { name: 'John', age: 23 }); console.log(man); // => { name: 'John', age: 23 } 複製代碼
利用 call
和 apply
:
let person = { name: null, sayName: function () { console.log(this.name); }, sayAge: function () { console.log(this.age); } }; let man = { name: 'Man', age: 23 }; person.sayName.call(man); // => Man person.sayAge.apply(man); // => 23 複製代碼
甚至咱們還可使用深拷貝對象的方式來完成相似繼承的操做……JS
中實現繼承的手法多種多樣,可是看看上面的代碼不難發現一些問題:
有沒有辦法解決這些問題呢?咱們可使用 JavaScript
中繼承最經常使用的方式:原型繼承
原型鏈繼承,就是讓對象實例經過原型鏈的方式串聯起來,當訪問目標對象的某一屬性時,能順着原型鏈進行查找,從而達到相似繼承的效果。
// 父類 function SuperType (colors = ['red', 'blue', 'green']) { this.colors = colors; } // 子類 function SubType () {} // 繼承父類 SubType.prototype = new SuperType(); // 以這種方式將 constructor 屬性指回 SubType 會改變 constructor 爲可遍歷屬性 SubType.prototype.constructor = SubType; let superInstance1 = new SuperType(['yellow', 'pink']); let subInstance1 = new SubType(); let subInstance2 = new SubType(); superInstance1.colors; // => ['yellow', 'pink'] subInstance1.colors; // => ['red', 'blue', 'green'] subInstance2.colors; // => ['red', 'blue', 'green'] subInstance1.colors.push('black'); subInstance1.colors; // => ['red', 'blue', 'green', 'black'] subInstance2.colors; // => ['red', 'blue', 'green', 'black'] 複製代碼
上述代碼使用了最基本的原型鏈繼承使得子類可以繼承父類的屬性,**原型繼承的關鍵步驟就在於:將子類原型和父類原型關聯起來,使原型鏈可以銜接上,**這邊是直接將子類原型指向了父類實例來完成關聯。
上述是原型繼承的一種最初始的狀態,咱們分析上面代碼,會發現仍是會有問題:
組合繼承使用 call
在子類構造函數中調用父類構造函數,解決了上述兩個問題:
// 組合繼承實現 function Parent(value) { this.value = value; } Parent.prototype.getValue = function() { console.log(this.value); } function Child(value) { Parent.call(this, value) } Child.prototype = new Parent(); const child = new Child(1) child.getValue(); child instanceof Parent; 複製代碼
然而它仍是存在問題:父類的構造函數被調用了兩次(建立子類原型時調用了一次,建立子類實例時又調用了一次),致使子類原型上會存在父類實例屬性,浪費內存。
針對組合繼承存在的缺陷,又進化出了「寄生組合繼承」:使用 Object.create(Parent.prototype)
建立一個新的原型對象賦予子類從而解決組合繼承的缺陷:
// 寄生組合繼承實現 function Parent(value) { this.value = value; } Parent.prototype.getValue = function() { console.log(this.value); } function Child(value) { Parent.call(this, value) } Child.prototype = Object.create(Parent.prototype, { constructor: { value: Child, enumerable: false, // 不可枚舉該屬性 writable: true, // 可改寫該屬性 configurable: true // 可用 delete 刪除該屬性 } }) const child = new Child(1) child.getValue(); child instanceof Parent; 複製代碼
寄生組合繼承的模式是如今業內公認的比較可靠的 JS
繼承模式,ES6
的 class
繼承在 babel
轉義後,底層也是使用的寄生組合繼承的方式實現的。
當咱們使用了原型鏈繼承後,怎樣判斷對象實例和目標類型之間的關係呢?
咱們可使用 instanceof
來判斷兩者間是否有繼承關係,instanceof
的字面意思就是:xx 是否爲 xxx 的實例。若是是則返回 true
不然返回 false
:
function Parent () {} function Child () {} Child.prototype = new Parent(); let parent = new Parent(); let child = new Child(); parent instanceof Parent; // => true child instanceof Child; // => true child instanceof Parent; // => true child instanceof Object; // => true 複製代碼
instanceof
本質上是經過原型鏈查找來判斷繼承關係的,所以只能用來判斷引用類型,對基本類型無效,咱們能夠手動實現一個簡易版 instanceof
:
function _instanceof (obj, Constructor) { if (typeof obj !== 'object' || obj == null) return false; let construProto = Constructor.prototype; let objProto = obj.__proto__; while (objProto != null) { if (objProto === construProto) return true; objProto = objProto.__proto__; } return false; } 複製代碼
還能夠利用 Object.prototype.isPrototypeOf
來間接判斷繼承關係,該方法用於判斷一個對象是否存在於另外一個對象的原型鏈上:
function Foo() {} function Bar() {} function Baz() {} Bar.prototype = Object.create(Foo.prototype); Baz.prototype = Object.create(Bar.prototype); var baz = new Baz(); console.log(Baz.prototype.isPrototypeOf(baz)); // true console.log(Bar.prototype.isPrototypeOf(baz)); // true console.log(Foo.prototype.isPrototypeOf(baz)); // true console.log(Object.prototype.isPrototypeOf(baz)); // true 複製代碼
本篇文章已收錄入 前端面試指南專欄