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