JavaScript "This" 大冒险:从小白到老司机
大家好,我是你们今天的导游,带大家一起探索 JavaScript 中最让人头疼,也最让人着迷的 "this" 指针。准备好了吗?系好安全带,我们出发!
很多新手刚开始接触 JavaScript 的时候,都会被 this
搞得晕头转向,觉得它像一个捉摸不透的幽灵,一会儿指向这里,一会儿指向那里。 别担心,今天我们要做的就是把这个幽灵彻底驯服,让它乖乖听话。
我们要学习 this
的四种绑定规则,以及箭头函数 (Arrow Functions) 对 this
的特殊处理。学完之后,保证你对 this
的理解能上一个台阶,以后再也不会被它坑啦!
1. Default Binding (默认绑定)
我们先从最简单的开始:默认绑定。 当 this
的绑定没有任何其他规则适用时,它就会采用默认绑定。 记住,默认绑定在严格模式和非严格模式下行为不同。
1.1 非严格模式
在非严格模式下,默认绑定的 this
指向全局对象。 在浏览器中,全局对象就是 window
;在 Node.js 中,全局对象是 global
。
function whatIsThis() {
console.log("This is:", this);
}
whatIsThis(); // 输出:This is: window (在浏览器中) 或 This is: global (在 Node.js 中)
这里,whatIsThis()
函数直接被调用,没有任何前缀,也没有被任何对象调用,因此 this
采用了默认绑定,指向了全局对象。
1.2 严格模式
为了代码的严谨性,JavaScript 引入了严格模式。在严格模式下,默认绑定的 this
指向 undefined
。
"use strict";
function whatIsThisStrict() {
console.log("This is:", this);
}
whatIsThisStrict(); // 输出:This is: undefined
可以看到,在严格模式下,this
不再指向全局对象,而是 undefined
。 这样做是为了避免一些意外的副作用,让你更清楚地知道 this
的指向。
总结:
模式 | 默认绑定 this 指向 |
---|---|
非严格模式 | 全局对象 (window 或 global ) |
严格模式 | undefined |
2. Implicit Binding (隐式绑定)
隐式绑定是指当函数被一个对象 "拥有" 并调用时,this
指向这个 "拥有" 函数的对象。 简单来说,就是看函数被谁调用。
const myObject = {
name: "小明",
sayHello: function() {
console.log(`Hello, my name is ${this.name}`);
}
};
myObject.sayHello(); // 输出:Hello, my name is 小明
在这个例子中,sayHello()
函数被 myObject
对象调用,所以 this
指向了 myObject
。 这就是隐式绑定。
2.1 链式调用
如果出现链式调用,this
指向的是链条上最后一个调用函数的对象。
const obj1 = {
name: "obj1",
obj2: {
name: "obj2",
sayName: function() {
console.log("My name is:", this.name);
}
}
};
obj1.obj2.sayName(); // 输出:My name is: obj2
这里,sayName()
函数是被 obj1.obj2
调用的,所以 this
指向 obj2
。
2.2 隐式丢失
隐式绑定有一个陷阱,叫做 "隐式丢失"。 当你把一个对象的函数赋值给另一个变量,或者把函数作为参数传递给另一个函数时,this
可能会丢失隐式绑定,退回到默认绑定。
const myObject = {
name: "小明",
sayHello: function() {
console.log(`Hello, my name is ${this.name}`);
}
};
const sayHelloAlias = myObject.sayHello;
sayHelloAlias(); // 输出:Hello, my name is undefined (非严格模式) 或 报错 (严格模式)
在这个例子中,我们将 myObject.sayHello
赋值给 sayHelloAlias
。 当我们调用 sayHelloAlias()
时,实际上是直接调用了函数,没有任何对象 "拥有" 它,所以 this
退回到了默认绑定。
另一个例子:
function doSomething(callback) {
callback();
}
const myObject = {
name: "小明",
sayHello: function() {
console.log(`Hello, my name is ${this.name}`);
}
};
doSomething(myObject.sayHello); // 输出:Hello, my name is undefined (非严格模式) 或 报错 (严格模式)
这里,我们将 myObject.sayHello
作为参数传递给 doSomething()
函数。 在 doSomething()
函数内部调用 callback()
时,this
同样丢失了隐式绑定,退回到了默认绑定。
总结:
规则 | 说明 |
---|---|
隐式绑定 | 函数被对象调用,this 指向该对象 |
链式调用 | this 指向链条上最后一个调用函数的对象 |
隐式丢失 | 函数赋值给变量或作为参数传递,可能丢失隐式绑定,退回默认绑定 |
3. Explicit Binding (显式绑定)
显式绑定允许你明确地指定函数执行时的 this
值。 JavaScript 提供了三个方法来实现显式绑定:call()
、apply()
和 bind()
。
3.1 call()
call()
方法允许你调用一个函数,并指定函数内部的 this
值,以及传递给函数的参数。
function sayHello(greeting) {
console.log(`${greeting}, my name is ${this.name}`);
}
const myObject = {
name: "小明"
};
sayHello.call(myObject, "你好"); // 输出:你好, my name is 小明
这里,我们使用 call()
方法调用 sayHello()
函数,并将 myObject
作为 this
值传递给它。 这样,sayHello()
函数内部的 this
就指向了 myObject
。
3.2 apply()
apply()
方法和 call()
方法类似,也允许你调用一个函数,并指定函数内部的 this
值。 唯一的区别是,apply()
方法接受一个数组作为参数,这个数组会被展开成函数的参数。
function sayHello(greeting, age) {
console.log(`${greeting}, my name is ${this.name}, I am ${age} years old.`);
}
const myObject = {
name: "小明"
};
sayHello.apply(myObject, ["你好", 18]); // 输出:你好, my name is 小明, I am 18 years old.
这里,我们使用 apply()
方法调用 sayHello()
函数,并将 myObject
作为 this
值传递给它,同时将 ["你好", 18]
数组作为参数传递给函数。
3.3 bind()
bind()
方法会创建一个新的函数,并将指定的 this
值绑定到这个新函数上。 这个新函数不会立即执行,而是返回一个绑定了 this
的函数,你可以稍后执行它。
function sayHello(greeting) {
console.log(`${greeting}, my name is ${this.name}`);
}
const myObject = {
name: "小明"
};
const boundSayHello = sayHello.bind(myObject);
boundSayHello("你好"); // 输出:你好, my name is 小明
这里,我们使用 bind()
方法创建了一个新的函数 boundSayHello
,并将 myObject
作为 this
值绑定到这个新函数上。 当我们调用 boundSayHello()
时,this
始终指向 myObject
。
3.4 硬绑定
bind()
创建的绑定是“硬绑定”,意味着即使你尝试使用 call()
或 apply()
改变 this
的值,也无法生效。
function sayHello() {
console.log("Hello, my name is " + this.name);
}
const person = { name: "Alice" };
const boundSayHello = sayHello.bind(person);
boundSayHello(); // 输出: Hello, my name is Alice
const anotherPerson = { name: "Bob" };
boundSayHello.call(anotherPerson); // 输出: Hello, my name is Alice (注意: 仍然是 Alice)
boundSayHello.apply(anotherPerson); // 输出: Hello, my name is Alice (注意: 仍然是 Alice)
总结:
方法 | 作用 | 参数 | 返回值 |
---|---|---|---|
call() |
调用函数,并指定 this 值和参数 |
第一个参数是 this 值,后面的参数是函数的参数 |
函数的返回值 |
apply() |
调用函数,并指定 this 值和参数 (参数以数组形式传递) |
第一个参数是 this 值,第二个参数是一个包含函数参数的数组 |
函数的返回值 |
bind() |
创建一个绑定了 this 值的新的函数 (不会立即执行) |
第一个参数是 this 值,后面的参数是函数的预设参数,这些参数会在调用新函数时自动传入 |
一个绑定了 this 值的新的函数,需要手动调用才能执行 |
4. New Binding (New 绑定)
当使用 new
关键字调用一个函数时,会发生以下几件事:
- 创建一个新的空对象。
- 将这个新对象的
[[Prototype]]
链接到构造函数的prototype
属性。 - 将这个新对象绑定到函数调用的
this
。 - 如果函数没有返回其他对象,则返回这个新对象。
因此,当使用 new
关键字调用函数时,this
指向这个新创建的对象。
function Person(name) {
this.name = name;
this.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};
}
const person1 = new Person("小明");
person1.sayHello(); // 输出:Hello, my name is 小明
const person2 = new Person("小红");
person2.sayHello(); // 输出:Hello, my name is 小红
在这个例子中,我们使用 new
关键字调用 Person()
函数,创建了两个新的对象 person1
和 person2
。 在 Person()
函数内部,this
指向了新创建的对象,因此我们可以设置对象的属性和方法。
总结:
规则 | 说明 |
---|---|
New 绑定 | 使用 new 关键字调用函数时,this 指向新创建的对象 |
5. 优先级
当多个绑定规则同时适用时,它们的优先级是什么样的呢? 记住这个优先级:
New Binding > Explicit Binding > Implicit Binding > Default Binding
New 绑定优先级最高,Default 绑定优先级最低。
function sayHello() {
console.log("Hello, my name is " + this.name);
}
const person1 = { name: "Alice" };
const person2 = { name: "Bob" };
const boundSayHello = sayHello.bind(person1); // Explicit Binding
boundSayHello(); // 输出: Hello, my name is Alice
new boundSayHello(); // 输出: Hello, my name is undefined (非严格模式) 或 报错 (严格模式) 因为new boundSayHello()忽略了bind的绑定.
function Person(name) {
this.name = name;
this.sayHello = function() {
console.log("Hello, my name is " + this.name);
}
}
const p = new Person("Charlie"); // New Binding
p.sayHello.call(person2); // 这里 call 并没有生效,因为 sayHello 中的 this 已经被 p 绑定了。
6. Arrow Functions (箭头函数)
箭头函数是 ES6 引入的一种新的函数语法。 箭头函数最大的特点就是它没有自己的 this
。 箭头函数会捕获其所在上下文的 this
值,并永久绑定到这个值。 这被称为 词法作用域 的 this
绑定。
const myObject = {
name: "小明",
sayHello: function() {
setTimeout(() => {
console.log(`Hello, my name is ${this.name}`);
}, 1000);
}
};
myObject.sayHello(); // 输出:Hello, my name is 小明 (1秒后)
在这个例子中,setTimeout()
函数内部的回调函数是一个箭头函数。 箭头函数捕获了 sayHello()
函数的 this
值,也就是 myObject
。 因此,箭头函数内部的 this
指向 myObject
。
如果不用箭头函数,代码会是这样的:
const myObject = {
name: "小明",
sayHello: function() {
const self = this; // 关键:用一个变量保存 this
setTimeout(function() {
console.log(`Hello, my name is ${self.name}`);
}, 1000);
}
};
myObject.sayHello(); // 输出:Hello, my name is 小明 (1秒后)
可以看到,使用箭头函数可以避免 this
丢失的问题,代码更加简洁易懂。
箭头函数不能使用 call()
、apply()
和 bind()
方法来改变 this
的值。 因为箭头函数的 this
是词法作用域的,一旦绑定就无法改变。
const myObject = {
name: "小明"
};
const sayHello = () => {
console.log(`Hello, my name is ${this.name}`);
};
sayHello.call(myObject); // 输出:Hello, my name is undefined (非严格模式) 或 报错 (严格模式)
sayHello.apply(myObject); // 输出:Hello, my name is undefined (非严格模式) 或 报错 (严格模式)
const boundSayHello = sayHello.bind(myObject);
boundSayHello(); // 输出:Hello, my name is undefined (非严格模式) 或 报错 (严格模式)
箭头函数也不能用作构造函数。 因为箭头函数没有自己的 this
,所以不能使用 new
关键字来创建对象。
const Person = (name) => {
this.name = name; // 报错: Cannot set properties of undefined (setting 'name')
};
const person = new Person("小明"); // 报错: Person is not a constructor
总结:
特点 | 说明 |
---|---|
没有自己的 this |
捕获其所在上下文的 this 值,并永久绑定 |
无法改变 this |
不能使用 call() 、apply() 和 bind() 方法来改变 this 的值 |
不能用作构造函数 | 因为没有自己的 this ,不能使用 new 关键字来创建对象 |
7. 总结
规则 | 描述 | 优先级 | 适用情况 |
---|---|---|---|
Default Binding | 函数独立调用,没有其他规则适用 | 最低 |
|
Implicit Binding | 函数被对象 "拥有" 并调用 | 较低 | 函数作为对象的属性被调用,this 指向该对象。 |
Explicit Binding | 使用 call() 、apply() 或 bind() 显式指定 this 值 |
较高 | 需要明确控制函数执行时的 this 值的情况。例如,将一个对象的函数借给另一个对象使用。 |
New Binding | 使用 new 关键字调用函数 |
最高 | 构造函数用于创建对象,this 指向新创建的对象。 |
Arrow Functions | 箭头函数没有自己的 this ,捕获其所在上下文的 this 值并永久绑定 |
N/A |
注意:箭头函数不能用作构造函数,也不能使用 |
希望通过这次 "This" 大冒险,你已经对 JavaScript 中的 this
指针有了更深入的理解。 记住这些规则,多加练习,你就能彻底驯服这个幽灵,让它为你所用! 祝你编程愉快!