原型鏈和繼承

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í)例换帜,明明什么都沒有做楔壤,就可以直接訪問toStringvalueOf等原生方法惯驼。那么這些方法是從什么地方來的呢蹲嚣?答案就是原型

image.png

在控制臺打印一個(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)的屬性/方法运杭。這就是問什么我們嘗試訪問空對象的toStringvulueOf等方法依舊能訪問到的原因,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屬性上

image.png

原型對象被創(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。這也證明了 ObjectJavaScript中數(shù)據(jù)類型的起源绑榴。

分析到這里哪轿,我們大概了解原型以及構(gòu)造函數(shù)的大概關(guān)系了,我們可以用一張圖來表示這個(gè)關(guān)系:

image.png

原型鏈

說完了原型翔怎,就可以來說說原型鏈餓了窃诉,如果理解原型機(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__是什么?如圖:

image.png

現(xiàn)在才想起來所有的函數(shù)可以使用new Function()的方式創(chuàng)建尤揣,那么這個(gè)答案也就很自然了搔啊,有點(diǎn)意思,再來試試別的構(gòu)造函數(shù)北戏。

image.png

這也證明了负芋,所有的函數(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.setPrototypeOfObject.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ì)之前解決原型污染問題的手法:

image.png

代碼很簡單凑术,只要是碰到有constructor或者__proto__這樣的敏感詞匯,就直接退出執(zhí)行了所意。這當(dāng)然是一種防止原型污染的有效手段淮逊,當(dāng)然我們還有其他手段:

  1. 使用Object.create(null), 方法創(chuàng)建一個(gè)原型為null的新對象扶踊,這樣無論對原型做什么擴(kuò)展都不會生效:
const obj = Object.create(null);
obj.__proto__ = { hack: '污染原型的屬性' };
console.log(obj); // => {}
console.log(obj.hack); // => undefined

  1. 使用````Object.freeze(obj)```凍結(jié)指定對象泄鹏,使之不能被修改屬性,成為不可擴(kuò)展對象:
Object.freeze(Object.prototype);

Object.prototype.toString = 'evil';

console.log(Object.prototype.toString);
// => ? toString() { [native code] }

  1. 建立JSON schema 秧耗,在解析用戶輸入內(nèi)容時(shí)备籽,通過JSON schema過濾敏感鍵名。
  2. 規(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 }

利用 callapply

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)還是會有問題:

  1. 在創(chuàng)建子類實(shí)例的時(shí)候,不能向超類型的構(gòu)造函數(shù)中傳遞參數(shù)问畅。
  2. 這樣創(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
如果幫組到您曹体,請舉小手手贊一下,筆芯 ???

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末盖奈,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子狐援,更是在濱河造成了極大的恐慌钢坦,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件啥酱,死亡現(xiàn)場離奇詭異爹凹,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)镶殷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進(jìn)店門禾酱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人绘趋,你說我怎么就攤上這事颤陶。” “怎么了陷遮?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵滓走,是天一觀的道長。 經(jīng)常有香客問我帽馋,道長搅方,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任绽族,我火速辦了婚禮姨涡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吧慢。我一直安慰自己涛漂,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布检诗。 她就那樣靜靜地躺著怖喻,像睡著了一般。 火紅的嫁衣襯著肌膚如雪岁诉。 梳的紋絲不亂的頭發(fā)上锚沸,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天,我揣著相機(jī)與錄音涕癣,去河邊找鬼哗蜈。 笑死前标,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的距潘。 我是一名探鬼主播炼列,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼音比!你這毒婦竟也來了俭尖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤洞翩,失蹤者是張志新(化名)和其女友劉穎稽犁,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體骚亿,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡已亥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了来屠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片虑椎。...
    茶點(diǎn)故事閱讀 40,680評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖俱笛,靈堂內(nèi)的尸體忽然破棺而出捆姜,到底是詐尸還是另有隱情,我是刑警寧澤迎膜,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布娇未,位于F島的核電站,受9級特大地震影響星虹,放射性物質(zhì)發(fā)生泄漏零抬。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一宽涌、第九天 我趴在偏房一處隱蔽的房頂上張望平夜。 院中可真熱鬧,春花似錦卸亮、人聲如沸忽妒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽段直。三九已至,卻和暖如春溶诞,著一層夾襖步出監(jiān)牢的瞬間鸯檬,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工螺垢, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留喧务,地道東北人赖歌。 一個(gè)月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像功茴,于是被迫代替她去往敵國和親庐冯。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評論 2 361