各位未来的代码大师们,晚上好!我是你们的老朋友,今天咱们来聊聊 JavaScript 里让人又爱又恨的 this
。 这玩意儿就像孙悟空的金箍棒,能大能小,变幻莫测,但掌握了它,就能在 JavaScript 的世界里所向披靡。
咱们今天的主题就是:JavaScript this
绑定的四大规则,以及箭头函数的特殊待遇。放心,我会尽量用大白话,配上实战代码,保证让你们听得懂、记得住、用得上。
一、this
是什么?为什么要研究它?
简单来说,this
就是一个指针,指向函数执行时的上下文。 说人话就是,this
代表函数执行时“谁”调用了它。
为什么要研究 this
? 因为它决定了函数内部能访问到哪些数据。 this
指向的对象不同,函数执行的结果可能完全不一样。 不理解 this
,就好像蒙着眼睛开车,迟早要翻车。
二、this
绑定的四大规则
this
的绑定规则,就像四位性格迥异的大佬,各有各的脾气,决定了 this
最终指向谁。
规则 | 描述 | 优先级 |
---|---|---|
Default | 如果没有任何规则适用,this 默认指向全局对象 (在浏览器中是 window ,在 Node.js 中是 global )。 |
最低 |
Implicit | 如果函数是被某个对象调用的,this 就指向那个对象。 |
中等 |
Explicit | 可以使用 call 、apply 或 bind 方法显式地指定 this 的值。 |
较高 |
New | 如果函数是通过 new 关键字调用的,this 就指向新创建的对象。 |
最高 |
接下来,咱们逐一击破。
1. Default Binding (默认绑定)
这是最宽松的规则,也是最容易被忽视的规则。 当函数独立调用,没有任何前缀对象,也没有使用 call
、apply
或 bind
时,this
就会指向全局对象。
function whoIsThis() {
console.log(this);
}
whoIsThis(); // 在浏览器中,输出 window 对象;在 Node.js 中,输出 global 对象
// 严格模式下,this 指向 undefined
function whoIsThisStrict() {
"use strict";
console.log(this);
}
whoIsThisStrict(); // 输出 undefined
注意事项:
- 严格模式下 (
"use strict"
),默认绑定会将this
设置为undefined
,而不是全局对象。 这有助于避免一些意外的全局变量污染。 - 在模块化环境中(例如使用 ES 模块或 CommonJS 模块),顶级
this
通常是undefined
,而不是全局对象。
2. Implicit Binding (隐式绑定)
这是最常用的规则。 如果函数是作为对象的方法调用的,this
就指向那个对象。
const myObject = {
name: "张三",
sayHello: function() {
console.log("Hello, 我是 " + this.name);
}
};
myObject.sayHello(); // 输出 "Hello, 我是 张三"
在这个例子中,sayHello
函数是被 myObject
对象调用的,所以 this
指向 myObject
。
更复杂一点的例子:
const person = {
name: "李四",
address: {
city: "北京",
getCity: function() {
console.log("我住在 " + this.city);
}
}
};
person.address.getCity(); // 输出 "我住在 北京"
这里 getCity
函数是被 person.address
对象调用的,所以 this
指向 person.address
。
容易出错的地方:
const person = {
name: "王五",
greet: function() {
console.log("你好,我是 " + this.name);
function innerFunction() {
console.log("我也是 " + this.name); // 这里的 this 指向哪里?
}
innerFunction(); // 独立调用,适用 Default Binding
}
};
person.greet(); // 输出 "你好,我是 王五" 和 "我也是 undefined" (或 "我也是 [全局对象属性 name 的值]")
在这个例子中,innerFunction
是独立调用的,而不是作为 person
对象的方法调用的。 因此,innerFunction
中的 this
适用默认绑定,指向全局对象 (或在严格模式下是 undefined
)。 想要让 innerFunction
中的 this
也指向 person
,需要使用 call
、apply
或 bind
,或者使用箭头函数 (后面会讲)。
再来一个经典面试题:
const counter = {
count: 0,
increment: function() {
setTimeout(function() {
this.count++;
console.log(this.count);
}, 1000);
}
};
counter.increment(); // 一秒后输出 NaN (或全局对象的 count 属性,如果存在)
console.log(counter.count); // 输出 0
为什么 counter.count
没有增加? 因为 setTimeout
内部的回调函数是独立调用的,this
适用默认绑定,指向全局对象 (或在严格模式下是 undefined
)。 我们需要显式地绑定 this
才能解决这个问题。
3. Explicit Binding (显式绑定)
call
、apply
和 bind
这三个方法,允许我们手动指定函数执行时的 this
值。 它们就像三个武林高手,可以强行改变 this
的指向。
call
: 接收一个this
值和若干个参数,参数之间用逗号分隔。apply
: 接收一个this
值和一个包含参数的数组。bind
: 接收一个this
值,并返回一个新的函数,该函数永久绑定了指定的this
值。
const person = {
name: "赵六"
};
function sayHello(greeting, punctuation) {
console.log(greeting + ", 我是 " + this.name + punctuation);
}
sayHello.call(person, "早上好", "!"); // 输出 "早上好, 我是 赵六!"
sayHello.apply(person, ["下午好", "?"]); // 输出 "下午好, 我是 赵六?"
const boundSayHello = sayHello.bind(person, "晚上好");
boundSayHello("~"); // 输出 "晚上好, 我是 赵六~"
call
和 apply
的区别:
仅仅在于传递参数的方式不同。 call
接收参数列表,apply
接收参数数组。
bind
的特点:
bind
返回一个新的函数,而不是立即执行。bind
可以预先设置一些参数,这些参数会在调用新函数时自动传入。bind
绑定的this
值是永久的,无法再次修改。
解决 setTimeout
问题的方案:
const counter = {
count: 0,
increment: function() {
setTimeout(function() {
this.count++;
console.log(this.count);
}.bind(this), 1000); // 使用 bind 绑定 this
}
};
counter.increment(); // 一秒后输出 1
console.log(counter.count); // 输出 0 (因为 setTimeout 是异步的)
或者:
const counter = {
count: 0,
increment: function() {
const self = this; // 保存 this 的引用
setTimeout(function() {
self.count++;
console.log(self.count);
}, 1000);
}
};
counter.increment(); // 一秒后输出 1
console.log(counter.count); // 输出 0 (因为 setTimeout 是异步的)
4. New Binding (New 绑定)
当使用 new
关键字调用函数时,会发生以下事情:
- 创建一个新的空对象。
- 将新对象的
__proto__
属性指向构造函数的prototype
属性。 - 将
this
绑定到新对象。 - 执行构造函数中的代码。
- 如果构造函数没有显式地返回一个对象,则返回新创建的对象。
function Person(name) {
this.name = name;
console.log("Person 函数中的 this:", this);
}
const person1 = new Person("田七"); // 输出 "Person 函数中的 this: Person {name: '田七'}"
console.log(person1.name); // 输出 "田七"
const person2 = new Person("小八"); // 输出 "Person 函数中的 this: Person {name: '小八'}"
console.log(person2.name); // 输出 "小八"
在这个例子中,new Person("田七")
创建了一个新的 Person
对象,并将 this
绑定到这个新对象。 因此,this.name = name
将 name
属性设置到了新对象上。
一个更复杂的例子:
function Animal(name) {
this.name = name;
this.sayHello = function() {
console.log("我是 " + this.name);
};
}
const dog = new Animal("旺财");
dog.sayHello(); // 输出 "我是 旺财"
三、优先级
当多种规则同时适用时,哪种规则说了算? 答案是:优先级高的规则胜出。
New Binding > Explicit Binding > Implicit Binding > Default Binding
function Person(name) {
this.name = name;
this.sayHello = function() {
console.log("Hello, 我是 " + this.name);
};
}
const obj = {
name: "老九"
};
const sayHello = new Person("孙十娘").sayHello.bind(obj);
sayHello(); // 输出 "Hello, 我是 孙十娘" (New Binding 优先级高于 Explicit Binding)
在这个例子中,虽然我们使用了 bind
将 this
绑定到 obj
,但 new Person("孙十娘")
已经将 this
绑定到了新创建的 Person
对象上。 由于 New Binding
的优先级高于 Explicit Binding
,所以最终 this
指向的是 Person
对象。
四、箭头函数的特殊处理
箭头函数是 ES6 引入的一种新的函数语法。 它与普通函数最大的区别在于,箭头函数没有自己的 this
值。 箭头函数会捕获其所在上下文的 this
值,并将其作为自己的 this
值。 也就是说,箭头函数的 this
值在定义时就已经确定,并且无法通过 call
、apply
或 bind
修改。
const person = {
name: "周十万",
greet: function() {
console.log("外层 this:", this); // 指向 person 对象
setTimeout(() => {
console.log("内层 this:", this); // 也指向 person 对象
console.log("你好,我是 " + this.name);
}, 1000);
}
};
person.greet(); // 一秒后输出 "你好,我是 周十万"
在这个例子中,箭头函数捕获了 greet
函数的 this
值,也就是 person
对象。 因此,箭头函数内部的 this
也指向 person
对象。 这解决了之前 setTimeout
的 this
指向问题。
箭头函数的适用场景:
- 当需要在函数内部访问外部函数的
this
值时。 - 当需要编写更简洁的代码时。
箭头函数的禁忌:
- 不要将箭头函数用作构造函数 (不能使用
new
关键字调用)。 - 不要将箭头函数用作对象的方法 (除非你明确知道你想要捕获外部的
this
值)。 - 不要在需要动态
this
值的情况下使用箭头函数。
箭头函数与 bind
:
由于箭头函数没有自己的 this
,所以 call
、apply
和 bind
方法对箭头函数无效。
const person = {
name: "吴承恩"
};
const sayHello = () => {
console.log("Hello, 我是 " + this.name);
};
sayHello.call(person); // 输出 "Hello, 我是 undefined" (或全局对象的 name 属性,如果存在)
sayHello.apply(person); // 输出 "Hello, 我是 undefined" (或全局对象的 name 属性,如果存在)
const boundSayHello = sayHello.bind(person);
boundSayHello(); // 输出 "Hello, 我是 undefined" (或全局对象的 name 属性,如果存在)
五、总结
this
的绑定规则是一个复杂但重要的概念。 掌握了这些规则,你就能更好地理解 JavaScript 的执行机制,编写更健壮、更可维护的代码。
回顾一下:
- Default Binding: 独立调用,
this
指向全局对象 (或在严格模式下是undefined
)。 - Implicit Binding: 对象调用,
this
指向对象。 - Explicit Binding:
call
、apply
、bind
,手动指定this
。 - New Binding:
new
关键字,this
指向新对象。 - 箭头函数: 没有自己的
this
,捕获上下文的this
。
记住这些规则,勤加练习,相信你很快就能成为 this
绑定的大师!
希望这次的讲座对大家有所帮助。 以后有机会,咱们再聊聊 JavaScript 的其他有趣话题。 祝大家编程愉快!