嘿,各位代码界的英雄们,晚上好!我是你们的老朋友,今天咱们来聊聊 JavaScript 中那个让人又爱又恨的 this
。这玩意儿就像个变色龙,一会儿指东,一会儿指西,搞得新手晕头转向。别怕,今天我就要扒光它的底裤,让它在咱们面前无所遁形!
咱们的目标是:用最接地气的方式,把 this
的五种绑定机制讲明白,让你以后再也不怕它出来捣乱。
this
:一个谜一样的男人(或女人)
首先,我们要明白,this
永远指向一个对象。问题是,它指向哪个对象?这就取决于函数是怎么被调用的。this
的指向是在运行时确定的,而不是定义时。
可以把 this
看作是函数的“上下文”,它代表了函数执行时所处的环境。
五大绑定机制:this
的变形术
下面,咱们就来逐一揭秘 this
的五种绑定机制,并配上生动的例子,保证让你一听就懂,一学就会。
-
默认绑定 (Default Binding)
这是最简单的一种情况,也是最容易被忽略的。当函数独立调用时,
this
会指向全局对象。在浏览器中,全局对象就是window
,而在 Node.js 中,全局对象是global
。function whoIsThis() { console.log("This is:", this); } whoIsThis(); // 在浏览器中,输出:This is: Window { ... } // 在 Node.js 中,输出:This is: Object [global] { ... } var myVar = "我是全局变量"; function checkGlobal() { console.log(this.myVar); } checkGlobal(); // 输出:我是全局变量
注意: 在严格模式 (
"use strict";
) 下,默认绑定会将this
绑定到undefined
,而不是全局对象。这是一种安全机制,可以防止意外地修改全局变量。"use strict"; function whoIsThisStrict() { console.log("This is:", this); } whoIsThisStrict(); // 输出:This is: undefined
总结: 默认绑定就像一个懒汉,啥也不管,直接把
this
指向全局对象(或者undefined
,如果开启了严格模式)。绑定类型 适用场景 this
指向是否受严格模式影响 默认绑定 函数独立调用,没有明确的上下文。 全局对象 (浏览器: window
, Node.js:global
)是 (指向 undefined
) -
隐式绑定 (Implicit Binding)
当函数作为某个对象的方法被调用时,
this
会指向该对象。换句话说,谁调用了函数,this
就指向谁。var myObject = { name: "张三", sayHello: function() { console.log("你好,我是" + this.name); } }; myObject.sayHello(); // 输出:你好,我是张三
在这个例子中,
sayHello
函数是myObject
对象的方法,因此this
指向myObject
。隐式绑定的一个常见陷阱:丢失绑定
var anotherObject = { name: "李四", myMethod: myObject.sayHello // 将 myObject 的 sayHello 方法赋值给 anotherObject 的 myMethod }; anotherObject.myMethod(); // 输出:你好,我是 undefined (或者全局对象上的 name 属性,如果存在)
为什么会这样?因为
anotherObject.myMethod()
实际上是独立调用了myMethod
函数,而不是作为myObject
的方法调用。因此,this
发生了丢失,指向了全局对象(或者undefined
,如果开启了严格模式)。解决方法:使用
bind
、call
或apply
强制绑定this
。 我们稍后会讲到这些方法。更隐蔽的丢失绑定:回调函数
var button = { text: "点击我", click: function() { console.log("按钮文本是:" + this.text); //期望输出: 按钮文本是:点击我 }, addEventListener: function(type, callback) { // 模拟 addEventListener callback(); // 实际执行时,this 指向 window (或 undefined,如果开启了严格模式) } }; button.addEventListener("click", button.click); //输出:按钮文本是:undefined (或者全局对象上的 text 属性,如果存在)
在这个例子中,
addEventListener
只是简单地调用了callback
函数,并没有指定this
的指向。因此,callback
函数中的this
仍然会指向全局对象(或者undefined
,如果开启了严格模式)。解决方法:
-
使用箭头函数:箭头函数没有自己的
this
,它会继承外层作用域的this
。var button = { text: "点击我", click: () => { // 使用箭头函数 console.log("按钮文本是:" + this.text); }, addEventListener: function(type, callback) { callback(); } }; button.addEventListener("click", button.click); //输出:按钮文本是:点击我
- 使用
bind
、call
或apply
强制绑定this
。
总结: 隐式绑定就像一个跟屁虫,函数在哪儿被调用,它就指向哪儿。但是要小心丢失绑定,尤其是在回调函数中。
绑定类型 适用场景 this
指向是否受严格模式影响 隐式绑定 函数作为对象的方法被调用。 调用函数的对象。 否 丢失绑定 隐式绑定的 this
指向丢失,变为默认绑定。全局对象 (浏览器: window
, Node.js:global
) 或undefined
是 (指向 undefined
) -
-
显式绑定 (Explicit Binding)
有时候,我们想要手动指定
this
的指向,这时候就可以使用显式绑定。JavaScript 提供了三个方法来实现显式绑定:call
、apply
和bind
。-
call
:call
方法可以调用一个函数,并指定该函数内部this
的指向,同时可以传递参数给该函数。function greet(greeting) { console.log(greeting + ",我是" + this.name); } var person = { name: "王五" }; greet.call(person, "你好"); // 输出:你好,我是王五
call
的第一个参数是要绑定的this
对象,后面的参数是传递给函数的参数列表。 -
apply
:apply
方法和call
方法类似,也可以调用一个函数,并指定该函数内部this
的指向,同时可以传递参数给该函数。不同的是,apply
接受一个包含参数的数组作为第二个参数。function greet(greeting, age) { console.log(greeting + ",我是" + this.name + ",今年" + age + "岁"); } var person = { name: "王五" }; greet.apply(person, ["你好", 30]); // 输出:你好,我是王五,今年30岁
apply
的第一个参数是要绑定的this
对象,第二个参数是一个包含参数的数组。 -
bind
:bind
方法会创建一个新的函数,并将该函数内部this
永久绑定到指定的对象。bind
方法不会立即执行函数,而是返回一个新的函数。function greet(greeting) { console.log(greeting + ",我是" + this.name); } var person = { name: "王五" }; var greetPerson = greet.bind(person); // 创建一个新的函数,并将 this 绑定到 person 对象 greetPerson("你好"); // 输出:你好,我是王五
bind
的第一个参数是要绑定的this
对象,后面的参数是传递给函数的预设参数。bind
的一个重要特性:柯里化bind
可以用来实现柯里化,也就是将一个接受多个参数的函数转换成一系列接受单个参数的函数。function multiply(a, b) { return a * b; } var multiplyByTwo = multiply.bind(null, 2); // 创建一个新的函数,预设第一个参数为 2 console.log(multiplyByTwo(5)); // 输出:10
在这个例子中,
multiply.bind(null, 2)
创建了一个新的函数multiplyByTwo
,它接受一个参数b
,并将a
固定为 2。
总结: 显式绑定就像一个遥控器,可以精确控制
this
的指向。call
和apply
立即执行函数,而bind
返回一个新的函数,方便以后调用。绑定类型 适用场景 this
指向是否受严格模式影响 显式绑定 需要手动指定 this
指向。call
、apply
或bind
指定的对象。否 -
-
new
绑定 (New Binding)当使用
new
关键字调用一个函数时,会发生以下几件事情:- 创建一个新的空对象。
- 将新对象的
__proto__
属性指向构造函数的prototype
属性。 - 将
this
绑定到新对象。 - 执行构造函数中的代码。
- 如果构造函数没有显式返回一个对象,则返回新对象。
function Person(name) { this.name = name; console.log("我被创建了,我的名字是:" + this.name); } var person1 = new Person("赵六"); // 输出:我被创建了,我的名字是:赵六 console.log(person1.name); // 输出:赵六
在这个例子中,
new Person("赵六")
创建了一个新的对象person1
,并将this
绑定到person1
。因此,this.name = name
将person1
的name
属性设置为 "赵六"。new
绑定的优先级高于隐式绑定。var obj = { name: "钱七", myMethod: function() { console.log("我的名字是:" + this.name); } }; var Person = function(name) { this.name = name; this.myMethod = obj.myMethod; // 将 obj 的 myMethod 赋值给 Person 的实例 }; var person1 = new Person("孙八"); person1.myMethod(); // 输出:我的名字是:孙八 (而不是钱七)
在这个例子中,
person1.myMethod()
实际上是调用了obj.myMethod
函数,但是由于person1
是通过new
关键字创建的,因此this
仍然指向person1
,而不是obj
。总结:
new
绑定就像一个造物主,它创建一个新的对象,并将this
绑定到这个新对象。绑定类型 适用场景 this
指向是否受严格模式影响 new
绑定使用 new
关键字调用函数(构造函数)。新创建的对象。 否 -
箭头函数 (Arrow Functions)
箭头函数是 ES6 引入的一种新的函数语法。箭头函数的一个重要特性是,它没有自己的
this
。箭头函数会继承外层作用域的this
,也就是定义时所处的上下文。var myObject = { name: "周九", sayHello: () => { console.log("你好,我是" + this.name); // this 指向外层作用域的 this (通常是 window 或 global) } }; myObject.sayHello(); // 输出:你好,我是 undefined (或者全局对象上的 name 属性,如果存在)
在这个例子中,
sayHello
是一个箭头函数,它没有自己的this
,因此this
指向外层作用域的this
,也就是全局对象(或者undefined
,如果开启了严格模式)。箭头函数非常适合用于回调函数,可以避免
this
丢失的问题。var button = { text: "点击我", click: function() { setTimeout(() => { console.log("按钮文本是:" + this.text); // this 指向 button 对象 }, 1000); } }; button.click(); // 一秒后输出:按钮文本是:点击我
在这个例子中,
setTimeout
接受一个箭头函数作为回调函数。由于箭头函数没有自己的this
,它会继承click
函数的this
,也就是button
对象。箭头函数不能用作构造函数,也不能使用
new
关键字调用。总结: 箭头函数就像一个寄生虫,它没有自己的
this
,而是依附于外层作用域的this
。绑定类型 适用场景 this
指向是否受严格模式影响 箭头函数 简化函数语法,避免 this
丢失。外层作用域的 this
。否
优先级:谁说了算?
当多种绑定机制同时存在时,this
的指向由优先级决定。优先级从高到低依次是:
new
绑定- 显式绑定 (
call
、apply
、bind
) - 隐式绑定
- 默认绑定
一个复杂的例子:
var obj = {
name: "对象A",
method: function() {
console.log("method - this.name:", this.name); // (2)
}
};
function globalFunction() {
this.name = "全局函数";
console.log("globalFunction - this.name:", this.name); // (1)
}
var newObj = new globalFunction(); // 使用 new 绑定
newObj.method = obj.method; // 隐式绑定
newObj.method(); // (3)
obj.method.call(window); // (4)
输出:
globalFunction - this.name: 全局函数 // (1) newObj 调用构造函数,this 指向 newObj
method - this.name: 对象A // (3) 隐式绑定,this 指向 newObj (因为方法是对象A 的 method 赋值给 newObj 的)
method - this.name: undefined // (4) 显示绑定, call 将 obj.method 的this 指向 window,window 没有 name 属性
避免 this
的陷阱:一些建议
- 始终明确
this
的指向: 在编写代码时,要时刻思考this
指向哪个对象。 - 避免全局变量污染: 尽量不要在全局作用域中定义变量,以免造成命名冲突。
- 使用箭头函数: 在回调函数中,尽量使用箭头函数,可以避免
this
丢失的问题。 - 善用
bind
、call
和apply
: 使用这些方法可以手动指定this
的指向,让代码更加灵活。 - 编写单元测试: 编写单元测试可以帮助你验证
this
的指向是否正确。
总结:this
,不再神秘!
通过今天的讲解,相信大家对 JavaScript 中 this
的五种绑定机制有了更深入的理解。this
就像一个谜题,需要我们认真分析函数是如何被调用的,才能找到正确的答案。只要掌握了这些规则,this
就不再是你的敌人,而是你的朋友!
希望今天的分享对大家有所帮助。下次再见!