Javascript天下第一 !
面試中經(jīng)常會被問到,new方法實現(xiàn)的原理,你能不能實現(xiàn)一個,在這個框架泛濫的年代,我還是決定沉下心來,自己在把基礎(chǔ)知識過一過,自己造一些輪子,沉淀沉淀。
說到new,call,bind的方法,就不得不從原型鏈開始說起
想起最早接觸原型鏈的時候,看起來特別頭疼,特別繞,后來也因為工作,經(jīng)常寫業(yè)務(wù)代碼,漸漸的對這些基礎(chǔ)就沒怎么用,而且用的也是很普通的給構(gòu)造函數(shù)上添加一個個的方法,更深曾的就沒去思考,去實現(xiàn)。
最近學(xué)習(xí)東西的時候喜歡帶著一些問題去學(xué)習(xí),去實現(xiàn)自己腦中的想法,然后去帶著問題去解決問題,接下來就先介紹下原型鏈的一些基礎(chǔ)知識,幫助大家理解下,可以更好的去實現(xiàn)這些內(nèi)置方法。介紹方法前,先把一些經(jīng)常會被問到的問題列出來,去學(xué)習(xí)的時候想著這些問題,去問問題,然后才能解決問題。
- 如何準(zhǔn)確判斷一個變量是數(shù)組
- 寫一個原型鏈繼承的例子
- 描述一下new一個對象的過程,手動實現(xiàn)一個(今天的主題)
問題挺多的,先寫這3個問題,接下來簡單介紹下原型鏈
這里我稱 __proto__ 為隱式原型,prototype稱為顯示原型,方便讀,方便交流
原型規(guī)則
所有的引用類型(數(shù)組,對象,函數(shù)),都具有對象特性:可自由擴(kuò)展屬性
所有的引用類型(數(shù)組,對象,函數(shù)),都有一個__ proto__屬性,屬性值是一個普通對象
所有的函數(shù),都有一個prototype屬性,屬性值是一個普通的對象
所有的引用類型(數(shù)組,對象,函數(shù)),__ proto__屬性指向它的構(gòu)造函數(shù)的prototype屬性值
當(dāng)試圖得到一個引用類型(數(shù)組,對象,函數(shù))的某個屬性時,如果這個對象本身并沒有這個屬性,那么會去它的__ proto__(也就是其構(gòu)造函數(shù)的prototype)上去查找
原型鏈大侄介紹這么多,更多原型鏈的這里就不贅述了。
new的實現(xiàn)
接下來我們來實現(xiàn)new方法,要實現(xiàn)一個方法,我們先思考這個方法做了什么事情,有輸入嗎,有輸出(返回)嗎,需要傳參嗎,參數(shù)類型有要求嗎,等...
我們來分析下,new的時候都做了什么事情
- 首先,我們會發(fā)現(xiàn)new完之后我們會得到一個對象,所以,new的時候會新創(chuàng)建一個空對象,并且最后會把我們創(chuàng)建好的對象給我們的變量,也就變成了一個實例了。
let obj = { };
- 然后我們發(fā)現(xiàn),這個new之后生成的對象(實例),它具有構(gòu)造函數(shù)的所有方法以及屬性。所以此時new操作做的事情就是把構(gòu)造函數(shù)上的所有屬性以及方法都賦值給咱們這個新創(chuàng)建的對象。
obj.__proto__ = fn.prototype fn指的是構(gòu)造函數(shù)
- 想下前面的操作,我們不是都拿到了構(gòu)造函數(shù)上面所有的方法了嗎,但是此時如果直接將這個新對象返回,我們得到的僅僅是一個包含構(gòu)造函數(shù)原型鏈上方法集合的一個對象。還缺少了當(dāng)前構(gòu)造函數(shù)內(nèi)部的屬性,所以需要把當(dāng)前的this值指向新的對象,來拿到構(gòu)造函數(shù)里的所有屬性,當(dāng)然,別忘了傳參數(shù)。
fn.apply(obj, arguments);
- 最后一步,將這個新的對象返回
return obj
總結(jié)
所以整合一下上面的思路,簡單來寫就是4步:
1)創(chuàng)建一個新對象
2)新對象的隱式原型鏈上掛載構(gòu)造函數(shù)的顯示原型鏈上的方法
3)把this指針指向新的對象
4)把對象返回
完整代碼
function _new() {
// 緩存一個arguments,避免直接修改污染arguments
let _arguments = arguments;
// 拿到構(gòu)造函數(shù),shift處理之后_arguments值已經(jīng)更改,shift會對原始數(shù)據(jù)有影響
let fn = Array.prototype.shift.call(_arguments)
// 第一步,創(chuàng)建一個對象
let target = {};
// 第二步,鏈接到原型,給新對象的隱式原型賦予構(gòu)造函數(shù)的顯示原型,得到構(gòu)造函數(shù)的所有prototype屬性值
target.__proto__ = fn.prototype;
// 第三步,把當(dāng)前的this指針指向新的對象
fn.apply(target, _arguments);
// 第四步,返回當(dāng)前this
return target;
}
call的實現(xiàn)
同樣的道理,我們先分析call做了什么事情
首先看下mdn上面對call的使用,它的語法是:
fun.call(thisArg, arg1, arg2, ...)
參數(shù):call方法可以接受很多參數(shù),其中第一個參為在 fun 函數(shù)運行時指定的 this 值,后面的是參數(shù)列表
作用:call方法提供新的 this 值給當(dāng)前調(diào)用的函數(shù)/方法
返回值:使用調(diào)用者提供的 this 值和參數(shù)調(diào)用該函數(shù)的返回值,如果該函數(shù)沒有返回值,則返回undefined,有返回值則返回值為該值
好了,清楚了call的使用方法和作用,咱們來看看它具體是怎么做到的:
- 首先,上面提到了this,參數(shù),被調(diào)用函數(shù),所以我們應(yīng)該拿到這些數(shù)值
let _this = Array.prototype.shift.call(arguments)
//這里將拿到傳過來的第一個參數(shù),并且改變了當(dāng)前的參數(shù)列表,這里拿到的是this值
//由于[].shift操作會改變原來的數(shù)組,所以當(dāng)前arguments剩下的值就是參數(shù)列表,不過是一個類數(shù)組
如何拿到被調(diào)用的函數(shù)呢,這里有兩個寫法,不過只是用法不同而已
fn._call(this)
//如果是這么調(diào)用的話,要獲得被調(diào)用函數(shù),那么就是當(dāng)前的this了,因為目前的_call方法是會掛到Function.prototype上
_call(fn,this)
//如果這么調(diào)用的話,就是取參數(shù)列表里面的第二個參數(shù)值了,看自己怎么傳參了~
這里我采用第一種寫法,畢竟emmm,比較方便
- 現(xiàn)在所需要的東西都有了,this,參數(shù),被調(diào)的函數(shù),那么剩下的就很簡單了,仔細(xì)想下call做了什么,不就是用新的指針去調(diào)用被調(diào)函數(shù)嗎,所以,看代碼:
_this.fn = this;
//_this是上面從參數(shù)列表里面拿到了的新的this指針
//this是當(dāng)前的方法,也就是被調(diào)用的函數(shù)
//這段代碼的意思是把需要被調(diào)用的函數(shù)掛載到新的指針下面
- 接下來就很簡單了,去執(zhí)行新的方法就好了,不過記得這里有返回值,所以直接返回這個函數(shù)執(zhí)行的結(jié)果就好哦~
return _this.fn(...arguments);
完整代碼
Function.prototype._call = function () {
// 不管什么操作,先緩存arguments,避免污染
let _arguments = arguments;
// 如果第一個參數(shù)是一個null,那么target為全局對象
// 拿到新的this對象,剩余的都是參數(shù)
let _this = Array.prototype.shift.call(_arguments);
if (_this == null) target = window;
// 拿到需要call的方法
// 第一步,給當(dāng)前的target下面添加所需要的方法
_this.fn = this;
// 第二步,把參數(shù)傳給target下面的函數(shù)
return _this.fn(...arguments);
}
//測試代碼
var foo = {
name: '張三'
}
function info(job, age) {
console.log('name', this.name)
console.log(job, age)
}
var name = 'window 張三'
info._call(foo,'web developer',24)
info._call(null,'web developer',24)
bind的實現(xiàn)
暫且下一期實現(xiàn)