javascript
中沒有類的概念馁龟,主要通過原型鏈來實(shí)現(xiàn)繼承怨酝。通常情況下傀缩,繼承意味著復(fù)制操作,然后js默認(rèn)并不會復(fù)制對象的屬性农猬,相反赡艰,js只是在兩個(gè)對象之間創(chuàng)建一個(gè)關(guān)聯(lián)(原型對象指針),這樣斤葱,一個(gè)對象就可以通過委托訪問另一個(gè)對象的屬性和函數(shù)瞄摊,所以與其叫做繼承勋又,委托的說法反而更加準(zhǔn)確些。
原型
當(dāng)我們 new 了一個(gè)新的對象實(shí)例换帜,明明什么都沒有做楔壤,就可以直接訪問
toString
、valueOf
等原生方法惯驼。那么這些方法是從什么地方來的呢蹲嚣?答案就是原型
在控制臺打印一個(gè)空對象的時(shí)候,我們可以看到祟牲,有很多方法隙畜,已經(jīng)“初始化”掛載在內(nèi)置的__proto__
對象上了。這個(gè)內(nèi)置的__proto__
是一個(gè)指向原型對象的指針说贝,它會創(chuàng)建一個(gè)新的引用類型對象時(shí)(顯示或者隱藏)自動創(chuàng)建议惰,并掛載到新的實(shí)例上。當(dāng)我們嘗試訪問實(shí)例對象上的某一屬性/方法時(shí)乡恕,如果實(shí)例對象上有該屬性/方法時(shí)言询,就會返回實(shí)例屬性/方法,如果沒有傲宜,就去__proto__
指向的原型對象上查找對應(yīng)的屬性/方法运杭。這就是問什么我們嘗試訪問空對象的toString
和vulueOf
等方法依舊能訪問到的原因,js正是以這種方式為基礎(chǔ)來實(shí)現(xiàn)繼承的函卒。
構(gòu)造函數(shù)
如果說實(shí)例的__proto__
只是一個(gè)指向原型對象的指針辆憔,那就說明在此之前原型對象就已經(jīng)創(chuàng)建來,那么原型對象是什么時(shí)候被創(chuàng)建的呢报嵌?這就要引入構(gòu)造函數(shù)的概念
// 普通函數(shù)
function person () {}
// 構(gòu)造函數(shù)虱咧,函數(shù)首字母通常大寫
function Person () {}
const person = new Person();
原型對象正是在構(gòu)造函數(shù)被聲明時(shí)一同被創(chuàng)建的,構(gòu)造函數(shù)被申明時(shí)锚国,原型對象也一同完成創(chuàng)建腕巡,然后掛載到構(gòu)造函數(shù)的prototype
屬性上
原型對象被創(chuàng)建時(shí),會自動生成一個(gè)constructor屬性跷叉,指向創(chuàng)建它的構(gòu)造函數(shù)逸雹。這樣它兩的關(guān)系就被緊密的關(guān)聯(lián)起來了营搅。
細(xì)心的話云挟,你就會發(fā)現(xiàn),原型對象也有自己的
__proto__
转质,這也不奇怪园欣,畢竟萬物皆有對象嘛。原型對象的__proto__
指向的是Object.prototype
休蟹。那么Object.prototype.__proto__
存不存在呢沸枯?其實(shí)是不存在的日矫,打印的話就會發(fā)現(xiàn)是null
。這也證明了Object
是JavaScript
中數(shù)據(jù)類型的起源绑榴。
分析到這里哪轿,我們大概了解原型以及構(gòu)造函數(shù)的大概關(guān)系了,我們可以用一張圖來表示這個(gè)關(guān)系:
原型鏈
說完了原型翔怎,就可以來說說原型鏈餓了窃诉,如果理解原型機(jī)制,原型鏈就很好解釋了赤套。其實(shí)在上面一張圖上飘痛,那條被__proto__
鏈接起來的鏈?zhǔn)疥P(guān)系,就稱為原型鏈容握。
原型鏈的作用: 原型鏈如此的重要的原因就在于它決定了js中繼承的實(shí)現(xiàn)方式宣脉。當(dāng)我們訪問一個(gè)屬性時(shí),查找機(jī)制如下:
- 訪問對象實(shí)例屬性剔氏,有則返回塑猖,沒有就通過
__proto__
去它的原型對象查找。 - 原型對象找到即返回介蛉,找不到萌庆,繼續(xù)通過原型對象的
__proto__
查找。 - 一層一層一直找到
Object.prototype
币旧,如果找到了目標(biāo)屬性即返回践险,找不到就返回undefined
,不會再往下找吹菱,因?yàn)樵谕抡?code>__proto__就是null
了巍虫。
通過上面的解釋,對于構(gòu)造函數(shù)生成的實(shí)例鳍刷,我們應(yīng)該能了解它的原型對象了占遥。JavaScript
中萬物皆有對象,那么構(gòu)造函數(shù)肯定也是個(gè)對象输瓜,是對象就有__proto__
瓦胎,那么構(gòu)造函數(shù)的__proto__
是什么?如圖:
現(xiàn)在才想起來所有的函數(shù)可以使用new Function()
的方式創(chuàng)建尤揣,那么這個(gè)答案也就很自然了搔啊,有點(diǎn)意思,再來試試別的構(gòu)造函數(shù)北戏。
這也證明了负芋,所有的函數(shù)都是Function的實(shí)例。那么Function.__proto__
嗜愈,那么Function.__proto__
豈不是旧蛾。莽龟。。
按照上面的邏輯锨天,這樣說的話毯盈,function
豈不是自己生成了自己?其實(shí)病袄,我們大可不必這樣理解奶镶,因?yàn)樽鳛橐粋€(gè)js``內(nèi)置對象,
function``` 對象在你的腳本文件都還沒有生成的時(shí)候就已經(jīng)存在了陪拘,哪里能自己調(diào)用自己厂镇。
至于為什么Function.__proto__
等于 Function.prototype
有這么幾種說法:
- 為了保持與其他函數(shù)保持一致
- 為了說明一種關(guān)系,比如證明所有的函數(shù)都是
function
的實(shí)例 - 函數(shù)都是可以調(diào)用
call
左刽、bind
這些內(nèi)置的api
的捺信,這么寫可以很好的保證函數(shù)實(shí)例能夠使用這些api
。
注意點(diǎn)
關(guān)于原型欠痴、原型鏈和構(gòu)造函數(shù)有幾點(diǎn)需要注意:
-__proto__
是非標(biāo)準(zhǔn)屬性迄靠,如果要訪問一個(gè)對象的原型,建議使用ES6新增的Reflect.getPrototypeOf
或者 Object.getPrototypeOf()
方法喇辽。同理掌挚,當(dāng)改變一個(gè)對象的原型時(shí),最好也使用ES6提供的Reflect.setPrototypeOf
或 Object.setPrototypeOf
菩咨。
let target = {};
let newProto = {};
Reflect.getPrototypeOf(target) === newProto; // false
Reflect.setPrototypeOf(target, newProto);
Reflect.getPrototypeOf(target) === newProto; // true
- 函數(shù)都會有
prototype
吠式,除了Function.prototype.bind()
之外。 - 對象都會有
__proto__
抽米,除了Object.prototype
之外(其實(shí)它也是有的特占,之不過是null)。 - 所有函數(shù)都是由
function
創(chuàng)建而來云茸,也就是說他們的__proto__
都等于Function.prototype
-
Function.prototype
等于Function.__proto__
原型污染
原型污染是指:攻擊者通過某種手段修改 JavaScript 對象的原型是目。
什么意思呢,原理其實(shí)很簡單标捺。如果我們把Object.prototype.toString
改成這樣:
Object.prototype.toString = function () {alert('原型污染')};
let obj = {};
obj.toString();
那么當(dāng)我們運(yùn)行這段代碼的時(shí)候?yàn)g覽器會彈出一個(gè)alert懊纳,對象原生的toString 方法被改成課,所有對象當(dāng)調(diào)用toString時(shí)都會受到影響亡容。
你可能說嗤疯,怎么可能有人傻到在源碼里寫這種代碼,這不是搬起石頭砸自己的腳嗎萍倡?沒錯身弊,沒有會在源碼里這么寫辟汰,但是攻擊者可能會通過表單或者修改請求內(nèi)容等方式使用原型污染發(fā)起攻擊列敲,來看下面一種情況:
'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}`);
如果服務(wù)器上有上述代碼的片段阱佛,攻擊者只要將cookie 設(shè)置成{proto: {admin: 1}} 就能完成系統(tǒng)的侵入。
原型污染的解決方案
在看原型污染的解決方案之前戴而,我們可以看下 lodash 團(tuán)隊(duì)之前解決原型污染問題的手法:
代碼很簡單凑术,只要是碰到有constructor
或者__proto__
這樣的敏感詞匯,就直接退出執(zhí)行了所意。這當(dāng)然是一種防止原型污染的有效手段淮逊,當(dāng)然我們還有其他手段:
- 使用
Object.create(null)
, 方法創(chuàng)建一個(gè)原型為null
的新對象扶踊,這樣無論對原型做什么擴(kuò)展都不會生效:
const obj = Object.create(null);
obj.__proto__ = { hack: '污染原型的屬性' };
console.log(obj); // => {}
console.log(obj.hack); // => undefined
- 使用````Object.freeze(obj)```凍結(jié)指定對象泄鹏,使之不能被修改屬性,成為不可擴(kuò)展對象:
Object.freeze(Object.prototype);
Object.prototype.toString = 'evil';
console.log(Object.prototype.toString);
// => ? toString() { [native code] }
- 建立
JSON schema
秧耗,在解析用戶輸入內(nèi)容時(shí)备籽,通過JSON schema
過濾敏感鍵名。 - 規(guī)避不安全的遞歸性合并分井,這一點(diǎn)類似
lodash
修復(fù)手段车猬,完善了合并操作的安全性,對敏感鍵名跳過處理尺锚。
繼承
概念:
繼承是面向?qū)ο筌浖夹g(shù)當(dāng)中的一個(gè)概念珠闰,與多態(tài)、封裝共為面向?qū)ο蟮娜齻€(gè)基本特征瘫辩。繼承可以使得子類具有父類的屬性和方法或者重新定義伏嗜、追加屬性和方法等。
這段對于程序員來說伐厌,這個(gè)解釋還是比較好理解的阅仔。接著:
子類的創(chuàng)建可以增加數(shù)據(jù)、新功能弧械,可以繼承父類的全部功能八酒,但是不能選擇性的繼承父類的部分功能。繼承是類與類之間的關(guān)系刃唐,不是對象與對象之間的關(guān)系羞迷。
這就尷尬了。js里哪來的類画饥,只有對象衔瓮。哪照這么說豈不是不能實(shí)現(xiàn)純正的繼承了?所以才會有開頭那句:與其叫繼承抖甘,委托的說話反而更準(zhǔn)確些热鞍。
但是js是非常靈活的,靈活這一特點(diǎn)給它帶來很多缺陷的同時(shí),也締造出很多驚艷的有點(diǎn)薇宠。沒有原生提供類的繼承不要緊偷办,我們可以用更多元的方式來實(shí)現(xiàn)js中的繼承,比如說利用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 中實(shí)現(xiàn)繼承的手法多種多樣澄港,但是看看上面的代碼不難發(fā)現(xiàn)一些問題:
- 分裝性不強(qiáng)椒涯,過于凌亂,寫起來十分不便回梧。
- 根本無法判斷子對象是何處繼承來的废岂。
有沒有辦法解決這些問題呢?我們可以使用js中繼承最常用的方式:原型繼承
原型鏈繼承
原型鏈繼承狱意,就是讓對象實(shí)例通過原型鏈的方式串聯(lián)起來湖苞,當(dāng)訪問目標(biāo)對象的某一屬性時(shí),能順著原型鏈進(jìn)行查找详囤,從而達(dá)到類似繼承的效果袒啼。
// 父類
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']
上述代碼使用了最基本的原型鏈繼承使的子類能夠繼承父類的屬性,原型繼承的關(guān)鍵步驟就在于:將子類原行和父類原型關(guān)聯(lián)起來纬纪,使原型鏈能夠銜接上蚓再,這邊是直接將子類原型指向了父類實(shí)例來完成關(guān)聯(lián)。
上述是原型繼承的一種最初始的狀態(tài)包各。我們分析上面的代碼摘仅,會發(fā)現(xiàn)還是會有問題:
- 在創(chuàng)建子類實(shí)例的時(shí)候,不能向超類型的構(gòu)造函數(shù)中傳遞參數(shù)问畅。
- 這樣創(chuàng)建的子類原型會包含父類的實(shí)例屬性娃属,造成引入類型屬性同步修改的問題。
組合繼承
組合繼承使用call在子類構(gòu)造函數(shù)中調(diào)用父類構(gòu)造函數(shù)护姆,解決了上述的問題:
// 組合繼承實(shí)現(xiàn)
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;
然而它還是存在問題:父類的構(gòu)造函數(shù)被調(diào)用了兩次(創(chuàng)建子類原型時(shí)調(diào)用了一次矾端,創(chuàng)建子類實(shí)例時(shí)又調(diào)用了一次),導(dǎo)致子類原型上會存在父類實(shí)例屬性卵皂,浪費(fèi)內(nèi)存秩铆。
寄生組合繼承
針對組合繼承存在的缺陷,又進(jìn)化出了“寄生組合繼承”:使用 Object.create(Parent.prototype)
創(chuàng)建一個(gè)新的原型對象賦予子類從而解決組合繼承的缺陷:
// 寄生組合繼承實(shí)現(xiàn)
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;
寄生組合繼承的模式是現(xiàn)在業(yè)內(nèi)公認(rèn)的比較可靠的 JS 繼承模式灯变,ES6 的 class
繼承在babel
轉(zhuǎn)義后殴玛,底層也是使用的寄生組合繼承的方式實(shí)現(xiàn)的。
繼承關(guān)系判斷
當(dāng)我們使用了原型鏈繼承后添祸,怎樣判斷對象實(shí)例和目標(biāo)類型之間的關(guān)系呢滚粟?
instanceof
我們可以使用 instanceof
來判斷二者間是否有繼承關(guān)系,instanceof
的字面意思就是:xx 是否為 xxx 的實(shí)例刃泌。如果是則返回 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
本質(zhì)上是通過原型鏈查找來判斷繼承關(guān)系的凡壤,因此只能用來判斷引用類型署尤,對基本類型無效,我們可以手動實(shí)現(xiàn)一個(gè)簡易版 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(obj)
還可以利用 Object.prototype.isPrototypeOf
來間接判斷繼承關(guān)系亚侠,該方法用于判斷一個(gè)對象是否存在于另一個(gè)對象的原型鏈上:
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
轉(zhuǎn)自:https://juejin.im/post/5eb52ad9e51d454de64e4306
如果幫組到您曹体,請舉小手手贊一下,筆芯 ???