徹底弄清 this call apply bind 以及原生實(shí)現(xiàn)
有關(guān) JS 中的 this、call、apply 和 bind 的概念網(wǎng)絡(luò)上已經(jīng)有很多文章講解了 這篇文章目的是梳理一下這幾個概念的知識點(diǎn)以及闡述如何用原生 JS 去實(shí)現(xiàn)這幾個功能
this 指向問題 thisthis 的指向在嚴(yán)格模式和非嚴(yán)格模式下有所不同;this 究竟指向什么是,在絕大多數(shù)情況下取決于函數(shù)如何被調(diào)用
全局執(zhí)行環(huán)境的情況:
非嚴(yán)格模式下,this 在全局執(zhí)行環(huán)境中指向全局對象(window、global、self);嚴(yán)格模式下則為 undefined
作為對象方法的調(diào)用情況:
假設(shè)函數(shù)作為一個方法被定義在對象中,那么 this 指向最后調(diào)用他的這個對象
比如:
a = 10 obj = { a: 1, f() { console.log(this.a) // this -> obj } } obj.f() // 1 最后由 obj 調(diào)用
obj.f() 等同于 window.obj.f() 最后由 obj 對象調(diào)用,因此 this 指向這個 obj
即便是這個對象的方法被賦值給一個變量并執(zhí)行也是如此:
const fn = obj.f fn() // 相當(dāng)于 window.fn() 因此 this 仍然指向最后調(diào)用他的對象 window
call apply bind 的情況:
想要修改 this 指向的時候,我們通常使用上述方法改變 this 的指向
a = 10 obj = { a: 1 } function fn(...args) { console.log(this.a, "args length: ", args) } fn.call(obj, 1, 2) fn.apply(obj, [1, 2]) fn.bind(obj, ...[1, 2])()
可以看到 this 全部被綁定在了 obj 對象上,打印的 this.a 也都為 1
new 操作符的情況:
new 操作符原理實(shí)際上就是創(chuàng)建了一個新的實(shí)例,被 new 的函數(shù)被稱為構(gòu)造函數(shù),構(gòu)造函數(shù) new 出來的對象方法中的 this 永遠(yuǎn)指向這個新的對象:
a = 10 function fn(a) { this.a = a } b = new fn(1) b.a // 1
箭頭函數(shù)的情況:
普通函數(shù)在運(yùn)行時才會確定 this 的指向
箭頭函數(shù)則是在函數(shù)定義的時候就確定了 this 的指向,此時的 this 指向外層的作用域
a = 10 fn = () => { console.log(this.a) } obj = { a: 20 } obj.fn = fn obj.fn() window.obj.fn() f = obj.fn f()
無論如何調(diào)用 fn 函數(shù)內(nèi)的 this 永遠(yuǎn)被固定在了這個外層的作用域(上述例子中的 window 對象)
this 改變指向問題如果需要改變 this 的指向,有以下幾種方法:
箭頭函數(shù)
內(nèi)部緩存 this
apply 方法
call 方法
bind 方法
new 操作符
箭頭函數(shù)普通函數(shù)
a = 10 obj = { a: 1, f() { // this -> obj function g() { // this -> window console.log(this.a) } g() } } obj.f() // 10
在 f 函數(shù)體內(nèi) g 函數(shù)所在的作用域中 this 的指向是 obj:
在 g 函數(shù)體內(nèi),this 則變成了 window:
改為箭頭函數(shù)
a = 10 obj = { a: 1, f() { // this -> obj const g = () => { // this -> obj console.log(this.a) } g() } } obj.f() // 1
在 f 函數(shù)體內(nèi) this 指向的是 obj:
在 g 函數(shù)體內(nèi) this 指向仍然是 obj:
內(nèi)部緩存 this這個方法曾經(jīng)經(jīng)常用,即手動緩存 this 給一個名為 _this 或 that 等其他變量,當(dāng)需要使用時用后者代替
a = 10 obj = { a: 20, f() { const _this = this setTimeout(function() { console.log(_this.a, this.a) }, 0) } } obj.f() // _this.a 指向 20 this.a 則指向 10
查看一下 this 和 _this 的指向,前者指向 window 后者則指向 obj 對象:
callcall 方法第一個參數(shù)為指定需要綁定的 this 對象;其他參數(shù)則為傳遞的值:
需要注意的是,第一個參數(shù)如果是:
null、undefined、不傳,this 將會指向全局對象(非嚴(yán)格模式下)
原始值將被轉(zhuǎn)為對應(yīng)的包裝對象,如 f.call(1) this 將指向 Number,并且這個 Number 的 [[PrimitiveValue]] 值為 1
obj = { name: "obj name" } {(function() { console.log(this.name) }).call(obj)}apply
與 call 類似但第二個參數(shù)必須為數(shù)組:
obj = { name: "obj name" } {(function (...args){ console.log(this.name, [...args]) }).apply(obj, [1, 2, 3])}bind
比如常見的函數(shù)內(nèi)包含一個異步方法:
function foo() { let _this = this // _this -> obj setTimeout(function() { console.log(_this.a) // _this.a -> obj.a }, 0) } obj = { a: 1 } foo.call(obj) // this -> obj // 1
我們上面提到了可以使用緩存 this 的方法來固定 this 指向,那么使用 bind 代碼看起來更加優(yōu)雅:
function foo() { // this -> obj setTimeout(function () { // 如果不使用箭頭函數(shù),則需要用 bind 方法綁定 this console.log(this.a) // this.a -> obj.a }.bind(this), 100) } obj = { a: 1 } foo.call(obj) // this -> obj // 1
或者直接用箭頭函數(shù):
function foo() { // this -> obj setTimeout(() => { // 箭頭函數(shù)沒有 this 繼承外部作用域的 this console.log(this.a) // this.a -> obj.a }, 100) } obj = { a: 1 } foo.call(obj) // this -> obj // 1new 操作符
new 操作符實(shí)際上就是生成一個新的對象,這個對象就是原來對象的實(shí)例。因?yàn)榧^函數(shù)沒有 this 所以函數(shù)不能作為構(gòu)造函數(shù),構(gòu)造函數(shù)通過 new 操作符改變了 this 的指向。
function Person(name) { this.name = name // this -> new 生成的實(shí)例 } p = new Person("oli") console.table(p)
this.name 表明了新創(chuàng)建的實(shí)例擁有一個 name 屬性;當(dāng)調(diào)用 new 操作符的時候,構(gòu)造函數(shù)中的 this 就綁定在了實(shí)例對象上
原生實(shí)現(xiàn) call apply bind new文章上半部分講解了 this 的指向以及如何使用 call bind apply 方法修改 this 指向;文章下半部分我們用 JS 去自己實(shí)現(xiàn)這三種方法
myCall首先 myCall 需要被定義在 Function.prototype 上這樣才能在函數(shù)上調(diào)用到自定義的 myCall 方法
然后定義 myCall 方法,該方法內(nèi)部 this 指向的就是 myCall 方法被調(diào)用的那個函數(shù)
其次 myCall 第一個參數(shù)對象中新增 this 指向的這個方法,并調(diào)用這個方法
最后刪除這個臨時的方法即可
代碼實(shí)現(xiàn):
Function.prototype.myCall = function(ctx) { ctx.fn = this ctx.fn() delete ctx.fn }
最基本的 myCall 就實(shí)現(xiàn)了,ctx 代表的是需要綁定的對象,但這里有幾個問題,如果 ctx 對象本身就擁有一個 fn 屬性或方法就會導(dǎo)致沖突。為了解決這個問題,我們需要修改代碼使用 Symbol 來避免屬性的沖突:
Function.prototype.myCall = function(ctx) { const fn = Symbol("fn") // 使用 Symbol 避免屬性名沖突 ctx[fn] = this ctx[fn]() delete ctx[fn] } obj = { fn: "functionName" } function foo() { console.log(this.fn) } foo.myCall(obj)
同樣的,我們還要解決參數(shù)傳遞的問題,上述代碼中沒有引入其他參數(shù)還要繼續(xù)修改:
Function.prototype.myCall = function(ctx, ...argv) { const fn = Symbol("fn") ctx[fn] = this ctx[fn](...argv) // 傳入?yún)?shù) delete ctx[fn] } obj = { fn: "functionName", a: 10 } function foo(name) { console.log(this[name]) } foo.myCall(obj, "fn")
另外,我們還要檢測傳入的第一個值是否為對象:
Function.prototype.myCall = function(ctx, ...argv) { ctx = typeof ctx === "object" ? ctx || window : {} // 當(dāng) ctx 是對象的時候默認(rèn)設(shè)置為 ctx;如果為 null 則設(shè)置為 window 否則為空對象 const fn = Symbol("fn") ctx[fn] = this ctx[fn](...argv) delete ctx[fn] } obj = { fn: "functionName", a: 10 } function foo(name) { console.log(this[name]) } foo.myCall(null, "a")
如果 ctx 為對象,那么檢查 ctx 是否為 null 是則返回默認(rèn)的 window 否則返回這個 ctx 對象;如果 ctx 不為對象那么將 ctx 設(shè)置為空對象(按照語法規(guī)則,需要將原始類型轉(zhuǎn)化,為了簡單說明原理這里就不考慮了)
執(zhí)行效果如下:
這么一來自定義的 myCall 也就完成了
另外修改一下檢測 ctx 是否為對象可以直接使用 Object;delete 對象的屬性也可改為 ES6 的 Reflect:
Function.prototype.myCall = function(ctx, ...argv) { ctx = ctx ? Object(ctx) : window const fn = Symbol("fn") ctx[fn] = this ctx[fn](...argv) Reflect.deleteProperty(ctx, fn) // 等同于 delete 操作符 return result }myApply
apply 效果跟 call 類似,將傳入的數(shù)組通過擴(kuò)展操作符傳入函數(shù)即可
Function.prototype.myApply = function(ctx, argv) { ctx = ctx ? Object(ctx) : window // 或者可以鑒別一下 argv 是不是數(shù)組 const fn = Symbol("fn") ctx[fn] = this ctx[fn](...argv) Reflect.deleteProperty(ctx, fn) // 等同于 delete 操作符 return result }myBind
bind 與 call 和 apply 不同的是,他不會立即調(diào)用這個函數(shù),而是返回一個新的 this 改變后的函數(shù)。根據(jù)這一特點(diǎn)我們寫一個自定義的 myBind:
Function.prototype.myBind = function(ctx) { return () => { // 要用箭頭函數(shù),否則 this 指向錯誤 return this.call(ctx) } }
這里需要注意的是,this 的指向原因需要在返回一個箭頭函數(shù),箭頭函數(shù)內(nèi)部的 this 指向來自外部
然后考慮合并接收到的參數(shù),因?yàn)?bind 可能有如下寫法:
f.bind(obj, 2)(2) // or f.bind(obj)(2, 2)
修改代碼:
Function.prototype.myBind = function(ctx, ...argv1) { return (...argv2) => { return this.call(ctx, ...argv1, ...argv2) } }
另外補(bǔ)充一點(diǎn),bind 后的函數(shù)還有可能會被使用 new 操作符創(chuàng)建對象。因此 this 理應(yīng)被忽略但傳入的參數(shù)卻正常傳入。
舉個例子:
obj = { name: "inner" // 首先定義一個包含 name 屬性的對象 } function foo(fname, lname) { // 然后定義一個函數(shù) this.fname = fname console.log(fname, this.name, lname) // 打印 name 屬性 } foo.prototype.age = 12
然后我們使用 bind 創(chuàng)建一個新的函數(shù)并用 new 調(diào)用返回新的對象:
boundf = foo.bind(obj, "oli", "young") newObj = new boundf()
看圖片得知,盡管我們定義了 obj.name 并且使用了 bind 方法綁定 this 但因使用了 new 操作符 this 被重新綁定在了 newObj 上。因此打印出來的 this.name 就是 undefined 了
因此我們還要繼續(xù)修改我們的 myBind 方法:
Function.prototype.myBind = function (ctx, ...argv1) { let _this = this let boundFunc = function (...argv2) { // 這里不能寫成箭頭函數(shù)了,因?yàn)橐褂?new 操作符會報錯 return _this.call(this instanceof boundFunc ? this : ctx, ...argv1, ...argv2) // 檢查 this 是否為 boundFunc 的實(shí)例 } return boundFunc }
然后我們使用看看效果如何:
this 指向問題解決了但 newObj 實(shí)例并未繼承到綁定函數(shù)原型中的值,因此還要解決這個問題,那么我們直接修改代碼增加一個 prototype 的連接:
Function.prototype.myBind = function (ctx, ...argv1) { let _this = this let boundFunc = function (...argv2) { return _this.call(this instanceof boundFunc ? this : ctx, ...argv1, ...argv2) } boundFunc.prototype = this.prototype // 連接 prototype 繼承原型中的值 return boundFunc }
看起來不錯,但還是有一個問題,嘗試修改 boundf 的原型:
發(fā)現(xiàn)我們的 foo 中原型的值也被修改了,因?yàn)橹苯邮褂?= 操作符賦值,其實(shí)本質(zhì)上還是原型的值,最后我們再修改一下,使用一個空的函數(shù)來重新 new 一個:
Function.prototype.myBind = function (ctx, ...argv1) { let _this = this let temp = function() {} // 定義一個空的函數(shù) let boundFunc = function (...argv2) { return _this.call(this instanceof temp ? this : ctx, ...argv1, ...argv2) } temp.prototype = this.prototype // 繼承綁定函數(shù)原型的值 boundFunc.prototype = new temp() // 使用 new 操作符創(chuàng)建實(shí)例并賦值 return boundFunc }
最后看下效果:
new 操作符最后我們再來實(shí)現(xiàn)一個 new 操作符名為 myNew
new 操作符的原理是啥:
生成新的對象
綁定 prototype (既然是 new 一個實(shí)例,那么實(shí)例的 __proto__ 必然要與構(gòu)造函數(shù)的 prototype 相連接)
綁定 this
返回這個新對象
代碼實(shí)現(xiàn):
function myNew(Constructor) { // 接收一個 Constructor 構(gòu)造函數(shù) let newObj = {} // 創(chuàng)建一個新的對象 newObj.__proto__ = Constructor.prototype // 綁定對象的 __proto__ 到構(gòu)造函數(shù)的 prototype Constructor.call(newObj) // 修改 this 指向 return newObj // 返回這個對象 }
然后考慮傳入?yún)?shù)問題,繼續(xù)修改代碼:
function myNew(Constructor, ...argv) { // 接收參數(shù) let newObj = {} newObj.__proto__ = Constructor.prototype Constructor.call(newObj, ...argv) // 傳入?yún)?shù) return newObj }小結(jié)
到此為止
this 指向問題
如何修改 this
如何使用原生 JS 實(shí)現(xiàn) call apply bind 和 new 方法
再遇到類似問題,基本常見的情況都能應(yīng)付得來了
(完)
參考:
https://juejin.im/post/59bfe8...
https://segmentfault.com/a/11...
https://github.com/Abiel1024/...
感謝 webgzh907247189 修改了一些代碼實(shí)現(xiàn)
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/108985.html
摘要:在實(shí)際開發(fā)項(xiàng)目中,有時我們會用到自定義按鈕因?yàn)橐粋€項(xiàng)目中,眾多的頁面,為了統(tǒng)一風(fēng)格,我們會重復(fù)用到很多相同或相似的按鈕,這時候,自定義按鈕組件就派上了大用場,我們把定義好的按鈕組件導(dǎo)出,在全局引用,就可以在其他組件隨意使用啦,這樣可以大幅度 在實(shí)際開發(fā)項(xiàng)目中,有時我們會用到自定義按鈕;因?yàn)橐粋€項(xiàng)目中,眾多的頁面,為了統(tǒng)一風(fēng)格,我們會重復(fù)用到很多相同或相似的按鈕,這時候,自定義按鈕組件就...
摘要:代碼整潔之道整潔的代碼不僅僅是讓人看起來舒服,更重要的是遵循一些規(guī)范能夠讓你的代碼更容易維護(hù),同時降低幾率。另外這不是強(qiáng)制的代碼規(guī)范,就像原文中說的,。里式替換原則父類和子類應(yīng)該可以被交換使用而不會出錯。注釋好的代碼是自解釋的。 JavaScript代碼整潔之道 整潔的代碼不僅僅是讓人看起來舒服,更重要的是遵循一些規(guī)范能夠讓你的代碼更容易維護(hù),同時降低bug幾率。 原文clean-c...
摘要:接著我之前寫的一篇有關(guān)前端面試題的總結(jié),分享幾道比較經(jīng)典的題目第一題考點(diǎn)作用域,運(yùn)算符栗子都會進(jìn)行運(yùn)算,但是最后之后輸出最后一個也就是那么其實(shí)就是而且是個匿名函數(shù),也就是屬于,就輸出第二和第三個都是類似的,而且作用域是都是輸出最后一個其實(shí)就 接著我之前寫的一篇有關(guān)前端面試題的總結(jié),分享幾道比較經(jīng)典的題目: 第一題: showImg(https://segmentfault.com/im...
對比內(nèi)容UCloudStackZStackVMwareQingCloud騰訊TStack華為云Stack優(yōu)勢總結(jié)?基于公有云自主可控?公有云架構(gòu)私有化部署?輕量化/輕運(yùn)維/易用性好?政府行業(yè)可復(fù)制案例輕量化 IaaS 虛擬化平臺?輕量化、產(chǎn)品成熟度高?業(yè)內(nèi)好評度高?功能豐富、交付部署快?中小企業(yè)案例多全套虛擬產(chǎn)品及云平臺產(chǎn)品?完整生態(tài)鏈、技術(shù)成熟?比較全面且健全的渠道?產(chǎn)品成熟度被市場認(rèn)可,市場占...
摘要:能跨平臺地設(shè)置及使用環(huán)境變量讓這一切變得簡單,不同平臺使用唯一指令,無需擔(dān)心跨平臺問題安裝方式改寫使用了環(huán)境變量的常見如在腳本多是里這么配置運(yùn)行,這樣便設(shè)置成功,無需擔(dān)心跨平臺問題關(guān)于跨平臺兼容,有幾點(diǎn)注意 cross-env能跨平臺地設(shè)置及使用環(huán)境變量, cross-env讓這一切變得簡單,不同平臺使用唯一指令,無需擔(dān)心跨平臺問題 1、npm安裝方式 npm i --save-de...
摘要:引入的模塊引入的使用將打包打包的拆分將一部分抽離出來物理地址拼接優(yōu)化打包速度壓縮代碼,這里使用的是,同樣在的里面添加 const path = require(path); //引入node的path模塊const webpack = require(webpack); //引入的webpack,使用lodashconst HtmlWebpackPlugin = require(ht...
閱讀 1794·2021-11-11 11:02
閱讀 1702·2021-09-22 15:55
閱讀 2503·2021-09-22 15:18
閱讀 3503·2019-08-29 11:26
閱讀 3759·2019-08-26 13:43
閱讀 2659·2019-08-26 13:32
閱讀 916·2019-08-26 10:55
閱讀 976·2019-08-26 10:27