this 關鍵字是 JavaScript 中最複雜的機制之一。它是一個很特別的關鍵字,被自動定義在所有函數的作用域中。但即使是非常有經驗的 JavaScript 開發者也很難說清它到底指向什麼。
任何足夠先進的技術都和魔法無異。 — Arthur C. Clarke
實際上,JavaScript 中this 的機制並沒有那麼先進,但是開發者往往會把理解過程複雜化, 毫無疑問,在缺乏清晰認識的情況下, this 對你來說完全就是一種魔法。
const obj = { title: '掘金', reading() { console.log(this.title + ',一个帮助开发者成长的社区'); } }
this 提供了一種更優雅的方式來隱式「傳遞」一個物件引用,因此可以將 API 設計得更加簡潔並且易於復用。
隨著你的使用模式越來越複雜,明確傳遞上下文物件會讓程式碼變得越來越混亂,使用 this 則不會這樣。當我們介紹物件和原型時,你就會明白函數可以自動引用合適的上下文物件有多重要
人們很容易把this 理解成指向函數本身,JavaScript 的新手開發者通常會認為,既然把函數看作一個物件(JavaScript 中的所有函數都是物件),那就可以在呼叫函數時儲存狀態(屬性的值)。但結果通常會讓他們大吃一驚,例如下面這段程式碼
function foo() { // 让新添加的 count + 1 this.count++ } // 向函数对象 foo 添加了一个属性 count foo.count = 0 foo() console.log(foo.count); // 0
這段程式碼看起來沒什麼問題,但請把目光聚焦到最後一行,輸出foo.count
的結果竟然是0 ? !
疑問:為什麼會這樣?我明明向函數物件foo
新增了一個屬性count
,並且函數內部也寫了this.count
,為什麼最後還是0
呢?
解答:因為this.count
中此時的this,根本不是指向foo 函數自身,而是指向全域window
。再細心一點,我們可以發現,在 window
身上,被加入了個 count
屬性,值為 NaN。
(為什麼 this 指向 window 後面會闡述)
所以,單純的把 this 理解為指向函數本身是錯誤的。
this 實際上是在函數被呼叫時發生的綁定,它指向什麼完全取決於函數在哪裡被呼叫。
我們來看看在函數的執行過程中呼叫位置如何決定 this 的綁定物件。
找到呼叫位置,然後判斷需要套用下面四條規則中的哪一條。我首先會分別解釋 這四條規則,然後解釋多條規則都可用時它們的優先順序如何排列。
首先要介紹的是最常用的函數呼叫類型:獨立函數呼叫。可以把這條規則看成是無法套用其他規則時的預設規則。
function foo() { console.log(this.a) } var a = 2 foo() // 2
我們可以看到當呼叫foo()
時,this.a
被解析變成全域變數a
。為什麼?因為在本例中,函數呼叫時應用了 this
的預設綁定,因此 this
指向全域物件。
那我們怎麼知道這裡應用了預設綁定呢?可以透過分析呼叫位置來看看 foo()
是如何調 用的。在程式碼中,foo()
是直接使用不帶任何修飾的函數參考進行呼叫的,因此只能使用預設綁定,無法應用其他規則
#這條規則也解釋了上面count 的程式碼中,為什麼函數裡面的this
指向了window
,因為foo
是屬於獨立函數呼叫的,觸發了預設綁定,從而指向全域window。 (瀏覽器中全域是window對象,node 中是空對象{})
#屬於預設綁定規則的還有:
(擴充:如果使用嚴格模式( strict mode),則不能將全域物件用於預設綁定,因此this 會綁定到undefined:)
結論:預設綁定的
this
,都指向全域。
这一条需要考虑的规则是调用位置是否有上下文对象,或者说是通过某个对象发起的函数调用
function foo() { console.log(this.a) } const obj = { a: 2, foo: foo } // 通过 obj 对象调用 foo 函数 obj.foo() // 2
调用位置会使用 obj 上下文来引用函数,因此你可以说函数被调用时 obj 对象“拥 有”或者“包含”它。
foo()
被调用时,它的前面确实加上了对 obj
的引用。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this
绑定到这个上下文对象。因为调用 foo()
时 this
被绑定到 obj
上,因此 this.a
和 obj.a
是一样的。
对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。举例来说:
function foo() { console.log(this.a) } var obj2 = { a: 2, foo: foo } var obj1 = { a: 1, obj2: obj2 } obj1.obj2.foo() // 2
最终 this
指向的是 obj2
一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上(取决于是否是严格模式)
第一种情况:将对象里的函数赋值给一个变量
function foo() { console.log(this.a) } var obj = { a: 2, foo: foo } var bar = obj.foo // 函数别名! var a = 'global' // a 是全局对象的属性 bar() // "global"
虽然 bar
是 obj.foo
的一个引用,但是实际上,它引用的是 foo
函数本身,因此此时的 bar()
其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
第二种情况:传入回调函数时
function foo() { console.log(this.a) } function doFoo(fn) { // fn 其实引用的是 foo fn() // <-- 调用位置! } var obj = { a: 2, foo: foo } var a = 'global' // a 是全局对象的属性 doFoo(obj.foo) // "global"
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一 个例子一样。
结论:隐式绑定的 this,指向调用函数的上下文对象。
如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?可以使用函数的 call(..)
和 apply(..)
方法
call(..)
和 apply(..)
方法。这两个方法是如何工作的呢?它们的第一个参数是一个对象,是给 this 准备的,接着在调用函数时将其绑定到 this。因为你可以直接指定 this 的绑定对象,因此我们称之为显式绑定。 思考以下代码:
function foo() { console.log(this.a) } var obj = { a: 2 } foo.call(obj) // 2
通过 foo.call(..)
,我们可以在调用 foo
时强制把它的 this
绑定到 obj
上。
如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对 象,这个原始值会被转换成它的对象形式(也就是 new String(..)、new Boolean(..) 或者 new Number(..))。这通常被称为“装箱”。
从 this 绑定的角度来说,call(..) 和 apply(..) 是一样的,它们的区别体现在参数上:第一个参数是相同的,后面的参数,call为参数列表,apply为数组,他们内部的实现原理也不难理解,详细请看以下两个手写方法
由于硬绑定是一种非常常用的模式,所以 ES5 提供了内置的方法 Function.prototype.bind
, 它的用法如下
function foo(num) { console.log(this.a, num) return this.a + num } var obj = { a: 2 } // 调用 bind() 方法,返回一个函数 var bar = foo.bind(obj) var b = bar(3) // 2 3 console.log(b) // 5
调用 bind(...)
方法,会返回一个新函数,那么这个新函数的 this
,永远指向我们传入的obj
对象
关于 bind 方法的简单实现,可以前往:手写 bind 方法,超级详细 ⚡⚡⚡
第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和 bind(..)
一样,确保你的回调函数使用指定的 this
。例如:
(1)数组方法 forEach()
function foo(el) { console.log(el, this.id) } var obj = { id: 'bin' }; [1, 2, 3].forEach(foo, obj) // 输出: // 1 bin // 2 bin // 3 bin
(2)setTimeout()
setTimeout(function() { console.log(this); // window }, 1000);
this
一般指向 window
,这个和 setTimeout 源码的内部调用有关,这个不再展开赘述结论:显式绑定的 this,指向我们指定的绑定对象。
在 JavaScript 中,普通函数可以使用 new
操作符去调用,此时的普通函数则被称为 “构造函数”。没错,凡是由 new
操作符调用的函数,都称为 “构造函数”
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
在内存中创建一个新对象。
这个新对象内部的[[Prototype]] 特性被赋值为构造函数的 prototype 属性。
构造函数内部的this 被赋值为这个新对象(即this 指向新对象)。
执行构造函数内部的代码(给新对象添加属性)。
如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
function foo(a) { this.a = a } var bar = new foo(2) console.log(bar.a) // 2
使用 new
来调用 foo(..)
时,我们会构造一个新对象并把它绑定到 foo(..)
调用中的 this
上。new
是最后一种可以影响函数调用时 this
绑定行为的方法,我们称之为 new 绑定。
结论:new 绑定的 this,都指向通过 new 调用的函数的实例对象(就是该函数)
现在我们已经了解了函数调用中 this 绑定的四条规则,你需要做的就是找到函数的调用位置并判断应当应用哪条规则。但是,如果某个调用位置可以应用多条规则呢?所以就需要有绑定规则的优先级。
它们之间的优先级关系为:
默认绑定 < 隐式绑定 < 显示绑定(bind) < new绑定
这里提前列出优先级,想看详细代码解析的可以往下看,也可以直接拖到最后面的例题部分
毫无疑问,默认规则的优先级是最低的,因为存在其他规则时,就会通过其他规则的方式来绑定this
测试一下即可知道,有以下代码:
function foo() { console.log(this.a) } var obj1 = { a: 1, foo: foo } var obj2 = { a: 2, foo: foo } // 同时使用隐式绑定和显示绑定 obj1.foo.call(obj2) // 2
可以看到,输出的结果为 2,说明 foo
函数内 this
指向的是 obj2
,而 obj2 是通过显示绑定调用的,所以:显示绑定的优先级更高
有以下测试代码:
function foo() { console.log(this); } var obj = { title: "juejin", foo: foo } // 同时使用隐式绑定和new绑定 new obj.foo(); // foo对象
最后 foo
函数输出的 this
为 foo 对象,说明new绑定优先级更高(否则应该输出 obj 对象),所以:new 绑定的优先级更高
最后,new 绑定和显式绑定谁的优先级更高呢?
new绑定和call、apply是不允许同时使用的,只能和 bind 相比较,如下:
function foo() { console.log(this) } var obj = { title: "juejin" } var foo = new foo.call(obj); // 直接报错
但是 new 绑定可以和 bind 方法返回后的函数一起使用
function foo() { console.log(this); } var obj = { title: "juejin" } var bar = foo.bind(obj); var foo = new bar(); // foo 对象, 说明使用的是new绑定
最后 foo
函数输出的 this
为 foo 对象,说明new绑定优先级更高(否则应该输出 obj 对象),所以:new 绑定的优先级更高
优先级结论:默认绑定 < 隐式绑定 < 显示绑定(bind)< new绑定
现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的 顺序来进行判断:
函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。 var bar = new foo()
函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是 指定的对象。 var bar = foo.call(obj2)
函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。 var bar = obj1.foo()
如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定 到全局对象。 var bar = foo()
就是这样。对于正常的函数调用来说,理解了这些知识你就可以明白 this 的绑定原理了。 不过……凡事总有例外
规则总有例外,这里也一样。在某些场景下 this 的绑定行为会出乎意料,你认为应当应用其他绑定规则时,实际上应用的可能是默认绑定规则。
箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this。
如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:
function foo() { console.log(this.a) } var a = 2 foo.call(null) // 2 foo.call(undefined) // 2 foo.bind(null)();
最后输出的结果都是 2,说明 this
指向的是全局 window
另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的间接引用,在这种情况下,调用这个函数会应用默认绑定规则。 间接引用最容易在赋值时发生:
function foo() { console.log(this.a) } var a = 2 var o = { a: 3, foo: foo } var p = { a: 4 } o.foo(); // 3 // 函数赋值 (p.foo = o.foo)() // 2
赋值表达式 p.foo = o.foo
的返回值是目标函数的引用,因此调用位置是 foo()
属于独立函数调用,而不是 p.foo()
或者 o.foo()
。根据我们之前说过的,这里会应用默认绑定。
请说出例题中的输出结果
var name = "window"; var person = { name: "person", sayName: function () { console.log(this.name); } }; function sayName() { var sss = person.sayName; sss(); person.sayName(); (person.sayName)(); (b = person.sayName)(); } sayName();
解析:
function sayName() { var sss = person.sayName; // 独立函数调用,没有和任何对象关联 sss(); // window // 关联 person.sayName(); // person (person.sayName)(); // person (b = person.sayName)(); // window }
var name = 'window' var person1 = { name: 'person1', foo1: function () { console.log(this.name) }, foo2: () => console.log(this.name), foo3: function () { return function () { console.log(this.name) } }, foo4: function () { return () => { console.log(this.name) } } } var person2 = { name: 'person2' } person1.foo1(); person1.foo1.call(person2); person1.foo2(); person1.foo2.call(person2); person1.foo3()(); person1.foo3.call(person2)(); person1.foo3().call(person2); person1.foo4()(); person1.foo4.call(person2)(); person1.foo4().call(person2);
解析:
// 隐式绑定,肯定是person1 person1.foo1(); // person1 // 隐式绑定和显示绑定的结合,显示绑定生效,所以是person2 person1.foo1.call(person2); // person2 // foo2()是一个箭头函数,不适用所有的规则 person1.foo2() // window // foo2依然是箭头函数,不适用于显示绑定的规则 person1.foo2.call(person2) // window // 获取到foo3,但是调用位置是全局作用于下,所以是默认绑定window person1.foo3()() // window // foo3显示绑定到person2中 // 但是拿到的返回函数依然是在全局下调用,所以依然是window person1.foo3.call(person2)() // window // 拿到foo3返回的函数,通过显示绑定到person2中,所以是person2 person1.foo3().call(person2) // person2 // foo4()的函数返回的是一个箭头函数 // 箭头函数的执行找上层作用域,是person1 person1.foo4()() // person1 // foo4()显示绑定到person2中,并且返回一个箭头函数 // 箭头函数找上层作用域,是person2 person1.foo4.call(person2)() // person2 // foo4返回的是箭头函数,箭头函数只看上层作用域 person1.foo4().call(person2) // person1
如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断 this 的绑定对象。
由 new 调用?绑定到新创建的对象。
由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
由上下文对象调用?绑定到那个上下文对象。
默认:在严格模式下绑定到 undefined,否则绑定到全局对象。
每文一句:如果把生活比喻为创作的意境,那么阅读就像阳光。
ok,本次的分享就到这里,如果本章内容对你有所帮助的话可以点赞+收藏,希望大家都能够有所收获。有任何疑问都可以在评论区留言,大家一起探讨、进步!
【推荐学习:javascript高级教程】
以上是帶你去詳解 this 的四個綁定規則的詳細內容。更多資訊請關注PHP中文網其他相關文章!