各位观众老爷,晚上好!我是你们的老朋友,代码界的段子手。今天咱们聊点刺激的——JavaScript 的 this
指向!
相信不少小伙伴都曾被 this
虐得死去活来,一会儿指向 window,一会儿指向按钮,一会儿又 undefined 了,简直比渣男还善变!今天,我就要带着大家从 Call Stack 的角度,扒一扒 this
动态绑定的底裤,保证让大家以后再也不怕 this
了。
开胃小菜:this
是个啥?
在正式开始之前,咱们先简单回顾一下 this
到底是个什么玩意儿。
简单来说,this
就是一个指针,指向函数执行时的执行上下文(Execution Context)。而执行上下文又包含了变量环境、词法环境、以及最重要的 this
绑定。
记住一句话:this
的指向,取决于函数是如何被调用的,而不是函数如何被定义的! 这就是 this
动态绑定的核心思想。
正餐:从 Call Stack 看 this
的动态绑定
好,开胃小菜吃完了,咱们上正餐!要理解 this
的动态绑定,就必须先了解 Call Stack。
1. 什么是 Call Stack?
Call Stack(调用栈)是 JavaScript 引擎追踪函数执行流程的一种数据结构。想象一下,它就像一摞盘子,每调用一个函数,就往上面放一个盘子(叫做 Stack Frame),函数执行完毕,就从上面拿掉一个盘子。
每个 Stack Frame 都包含了函数的信息,比如函数名、参数、以及最重要的 执行上下文。而 this
就在这个执行上下文中。
2. Call Stack 如何影响 this
的指向?
关键就在于:当函数被调用时,JavaScript 引擎会根据调用方式,来确定当前 Stack Frame 的 this
绑定。
下面我们来分情况讨论:
(1) 默认绑定(Default Binding):全局环境 or undefined
这是最简单的情况,当函数独立调用(也就是直接 func()
这样调用)时,this
的指向取决于是否是严格模式。
- 非严格模式:
this
指向全局对象,在浏览器中通常是window
。 - 严格模式:
this
指向undefined
。
function foo() {
console.log(this);
}
foo(); // 非严格模式: window, 严格模式: undefined
"use strict";
function bar() {
console.log(this);
}
bar(); // undefined
Call Stack 分析:
- 非严格模式: 当调用
foo()
时,JavaScript 引擎创建一个 Stack Frame,由于是独立调用,引擎会将this
绑定到全局对象window
。 - 严格模式: 当调用
bar()
时,JavaScript 引擎创建一个 Stack Frame,由于是严格模式下的独立调用,引擎会将this
绑定到undefined
。
(2) 隐式绑定(Implicit Binding):谁调用我,我就指向谁
当函数作为对象的方法被调用时,this
指向调用该方法的对象。
const obj = {
name: "小明",
sayHello: function() {
console.log(`Hello, I'm ${this.name}`);
}
};
obj.sayHello(); // Hello, I'm 小明
Call Stack 分析:
- 当调用
obj.sayHello()
时,JavaScript 引擎创建一个 Stack Frame。 - 引擎发现
sayHello
是作为obj
的方法被调用的,因此将this
绑定到obj
。
注意:隐式绑定可能会丢失!
const obj = {
name: "小明",
sayHello: function() {
console.log(`Hello, I'm ${this.name}`);
}
};
const greet = obj.sayHello; // 将方法赋值给一个变量
greet(); // Hello, I'm undefined (非严格模式) 或 报错 (严格模式)
Call Stack 分析:
- 当
greet()
被调用时,JavaScript 引擎创建一个 Stack Frame。 - 虽然
greet
之前引用了obj.sayHello
,但现在greet
是被独立调用的,所以this
遵循默认绑定规则,指向全局对象或undefined
。
(3) 显式绑定(Explicit Binding):call、apply、bind
显式绑定允许我们手动指定 this
的指向。
call
和apply
: 立即执行函数,并指定this
的指向。两者的区别在于参数传递方式不同。call
接收参数列表,apply
接收参数数组。
function greet(greeting) {
console.log(`${greeting}, ${this.name}`);
}
const obj = { name: "小红" };
greet.call(obj, "你好"); // 你好, 小红
greet.apply(obj, ["Hello"]); // Hello, 小红
Call Stack 分析:
-
当
greet.call(obj, "你好")
或greet.apply(obj, ["Hello"])
被调用时,JavaScript 引擎创建一个 Stack Frame。 -
call
和apply
强制将this
绑定到obj
。 -
bind
: 创建一个新的函数,并将this
永久绑定到指定对象。
function greet(greeting) {
console.log(`${greeting}, ${this.name}`);
}
const obj = { name: "小刚" };
const boundGreet = greet.bind(obj); // 创建一个新函数, this 绑定到 obj
boundGreet("Hi"); // Hi, 小刚
Call Stack 分析:
- 当
greet.bind(obj)
被调用时,bind
方法创建一个新的函数boundGreet
。 boundGreet
内部的this
已经被永久绑定到obj
,即使以后boundGreet
被独立调用,this
仍然指向obj
。
(4) new 绑定(New Binding):构造函数
当使用 new
关键字调用函数时,会发生以下几个步骤:
- 创建一个新的空对象。
- 将新对象的原型指向构造函数的
prototype
属性。 - 将
this
绑定到新对象。 - 执行构造函数中的代码。
- 如果构造函数没有显式返回一个对象,则返回新对象。
function Person(name) {
this.name = name;
console.log(this); // 指向新创建的 Person 实例
}
const person = new Person("小强"); // Person {name: "小强"}
console.log(person.name); // 小强
Call Stack 分析:
- 当
new Person("小强")
被调用时,JavaScript 引擎创建一个 Stack Frame。 new
关键字强制将this
绑定到新创建的Person
实例。
优先级:显式绑定 > 隐式绑定 > 默认绑定
当多种绑定规则同时存在时,优先级如下:
new
绑定 > 显式绑定 > 隐式绑定 > 默认绑定
function foo(something) {
this.a = something;
return this;
}
var obj1 = {};
var obj2 = {};
// 隐式绑定
foo.call( obj1, "a" ); // 显式绑定
obj2 = foo.bind( obj1, "b" );
obj2("c");
console.log( obj1.a ); // b
表格总结:this
的指向
调用方式 | this 指向 |
说明 |
---|---|---|
独立调用 | 全局对象 (非严格模式) 或 undefined (严格模式) |
遵循默认绑定规则。 |
作为对象的方法调用 | 调用该方法的对象 | 遵循隐式绑定规则。 |
call / apply |
指定的对象 | 遵循显式绑定规则。 |
bind |
指定的对象 | 创建一个新的函数,并将 this 永久绑定到指定对象。 |
new |
新创建的对象 | 遵循 new 绑定规则。 |
箭头函数 | 继承自外层作用域的 this |
箭头函数没有自己的 this ,它会捕获其所在上下文的 this 值,作为自己的 this 值。这意味着箭头函数中的 this 值在定义时就已经确定,并且永远不会改变。这使得箭头函数非常适合用于回调函数,因为它们可以避免 this 指向问题。但是,这也意味着箭头函数不能用作构造函数,因为它们没有自己的 this 值。 |
彩蛋:箭头函数的 this
箭头函数是个特殊的存在,它没有自己的 this
,它的 this
是继承自外层作用域的 this
。
const obj = {
name: "小李",
sayHello: function() {
setTimeout(() => {
console.log(`Hello, I'm ${this.name}`);
}, 1000);
}
};
obj.sayHello(); // Hello, I'm 小李
Call Stack 分析:
- 当
obj.sayHello()
被调用时,this
指向obj
。 setTimeout
接收一个箭头函数作为回调函数。- 箭头函数没有自己的
this
,它会捕获sayHello
函数的this
,也就是obj
。
总结:this
的终极奥义
理解 this
的关键在于:
- 明确函数的调用方式 (独立调用、作为对象方法调用、
call
/apply
/bind
、new
)。 - 理解 Call Stack 的作用,知道 JavaScript 引擎是如何根据调用方式来确定
this
绑定的。 - 记住优先级规则。
只要掌握了这三点,this
就不再是你的噩梦,而是你手中的利器!
练习题:
var name = "Global";
var obj = {
name: "Obj",
getName: function () {
return function () {
return this.name;
};
}
};
console.log(obj.getName()()); // 输出什么?
答案是 "Global"。
解释:
obj.getName()
返回一个匿名函数。- 这个匿名函数被立即调用
()
,但它是独立调用,不是作为obj.getName()
的方法调用,因此this
指向全局对象 (window)。 - 全局对象的
name
属性是 "Global"。
希望今天的分享能帮助大家彻底理解 JavaScript 的 this
指向。记住,代码虐我千百遍,我待代码如初恋!下次再见!