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