主播这几个月在实习,每天下班后根本不想写文章所以断更了几个月。最近秋招开始了,也准备开始投简历并复习复习八股了,越复习到后面越发现要搞懂函数柯里化等难一点的知识点还是得要把this给搞明白,于是开始查看各种资料,准备把我的理解分享给大家,希望对大家有帮助。
一、this是什么
1.面向对象与"this"
在JavaScript中,我们通常用创建对象来表示真实世界中的实体如人与动物等:
js
let people = {
name: "Karina",
age: 20,
}
并且,在现实世界中,人也可以进行"操作":比如吃饭,运动。在JavaScript中,行为(action)由属性中的函数来表示。
方法示例 刚开始,我们来教people说hello:
js
let people = {
name: "Karina",
age: 20,
}
people.sayHello = function () {
console.log("Hello!")
}
people.sayHello() // Hello!
在这里我们使用函数表达式创建了一个函数,并将其指定给对象的people.sayHello属性。随后我们像people.sayHello()这样调用它。人现在学会说话了!
作为对象属性的函数被称之为方法 。所以,在这里我们得到了people对象的sayHello方法。当然,我们也可以使用预先声明的函数作为方法,就像这样:
js
let people = {
// ...
}
// 首先声明函数
function sayHello() {
console.log("Hello!")
}
// 然后将其作为一个方法添加
people.sayHello = sayHello
// 调用方法
people.sayHello() // Hello!
2.方法中的"this"
通常,对象方法需要访问对象中存储的信息才可以完成其工作。例如people.sayHello()中的代码可能需要用到people的name属性。为了访问该对象,方法中可以使用this关键字。this的值就是点之前的这个对象,即调用该方法的对象。
举个例子:
js
let people = {
name: "Karina",
age: 20,
sayHello() {
// "this"指的是"当前的对象"
console.log("this.name")
}
}
// 调用方法
people.sayHello() // Karina
在这里people.sayHello()执行过程中,this的值是people。技术上讲,我们也可以在不使用this的情况下,通过外部变量名来引用它:
js
let people = {
name: "Karina",
age: 20,
sayHello() {
console.log(people.name) // people代替this
}
}
但这样的代码是极其不靠谱的。如果我们将people复制给另一个变量例如 cat = people,并赋另外的值给people,那么它将访问到错误的对象。
我们来看个例子
js
let people = {
name: "Karina",
age: 20,
sayHello() {
console.log(people.name)
}
}
let cat = people
people = null
cat.sayHello() // TypeError: Cannot read properties of null (reading 'name')
如果我们在console.log中把this.name替换成people.name,那么代码就可以正常运行。
3.自由的"this"不受限制
在JavaScript中,this关键字与大部分编程语言中的不同,JavaScript中的this可以用于任何函数,即使它不是对象的方法。
下面这段代码就没有语法错误
js
function sayHello() {
console.log(this.name)
}
this的值是在代码运行时计算出来的,它取决于代码上下文。
例如这里相同的函数被分配给两个不同的对象,在调用中有着不同的this值:
js
let man = { name: "jack" }
let woman = { name: "lily" }
function sayHello() {
console.log(this.name)
}
// 在两个对象中使用相同的函数
man.sayHello = sayHello
woman.sayHello = sayHello
// 这两个调用有不同的this值
// 函数内部的this是点符号前面的那个对象
man.sayHello() // jack
woman.sayHello() // lily
在没有对象的情况下调用: this === undefined
我们可以在没有对象的情况下调用函数:
js
function sayHello() {
console.log(this);
}
sayHello(); // undefined
在这种情况下,严格模式下的this值为undefined。如果我们尝试访问this.name,将会报错。在非严格模式下,this将会是全局对象(浏览器中的window)。
这种调用通常是程序出错了,如果在一个函数内部有this,那么通常意味着它是在对象上下文环境中被调用的。
二、this的绑定规则
1."this"的定义
在上面我们已经知道了,this关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象。
举个例子
js
function baz() {
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域
console.log("baz");
bar(); // <-- bar的调用位置
}
function bar() {
// 当前的调用栈是:baz --> bar
// 因此,当前调用位置是baz
console.log("bar");
foo(); // <-- foo的调用位置
}
function foo() {
// 当前调用栈是:baz --> bar --> foo
// 因此,当前调用位置是bar
console.log("foo");
}
baz(); // <-- baz的调用位置
同时,this在函数执行过程中,this一旦被确定了,就不可以再更改。
js
var a = 10;
var obj = {
a: 20
}
function fn() {
this = obj; // 修改this,运行后会报错
console.log(this.a);
}
fn();
fn()在执行时函数在全局被调用,this此时执行window,但是在运行过程中把this指向obj此时就会报错。
2."this"的四种绑定方式
- 默认绑定
- 隐式绑定
new绑定- 显示绑定
默认绑定
直接调用函数时(如func()),this指向全局对象(非严格模式下)或undefined(严格模式下)。
js
var name = 'Jenny';
function person() {
return this.name;
}
console.log(person()); //Jenny
全局环境下定义person函数,内部使用this关键字。上述代码输出Jenny,原因是调用函数的对象在浏览器中位于window,因此this指向window,所以输出Jenny。
特别注意:
严格模式下,不能将全局对象用于默认绑定,this会绑定到undefined,只有函数运行在非严格模式下,默认绑定才能绑定到全局对象。
隐式绑定
函数可以作为某个对象的方法调用,这时this就指这个上级对象
js
function test() {
console.log(this.x);
}
var obj = {};
obj.x = 1;
obj.m = test;
obj.m(); // 1`
这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象
js
var o = {
a:10,
b:{
fn:function(){
console.log(this.a); //undefined
}
}
}
o.b.fn();
上述代码中,this的上一级对象为b,b内部并没有a变量的定义,所以输出undefined
我们再来看一种特殊情况!
js
var o = {
a:10,
b:{
a:12,
fn:function(){
console.log(this.a); //undefined
console.log(this); //window
}
}
}
var j = o.b.fn;
j();
我们来分析一下
函数fn最初定义在o.b对象中,即o.b.fn = function() {...}。但此时函数并未执行,this的执行还未确定(this只有在函数调用时才会确定)。var j = o.b.fn; 这行代码是将fn函数本身赋值给变量j,相当于j现在是函数fn的一个引用 (变量j保存了函数的地址),但j与原对象o.b已经没有关联了。
当执行j()时,本质上是通过全部变量j调用函数。在JavaScript中:
- 当函数独立调用时(即没有明确的调用对象时),
this会默认绑定到全局对象(浏览器环境是window。Node环境是global)。 - 此时
j()等价于window.j()(浏览器环境),因此函数内部的this指向window。
小结一下
this的指向取决于函数的调用方式,而非定义位置。
new绑定
构造函数的本质
构造函数的目的是创建并初始化一个新对象 。new操作符的底层逻辑包括以下步骤:
- 创建新对象 :生成一个空对象
{}。 - 绑定原型 :将新对象的
__proto__指向构造函数的prototype属性。 - 绑定this :将构造函数内部的
this指向新对象。 - 执行构造函数:执行构造函数体,初始化新对象的属性和方法。
- 返回对象:如果构造函数没有显式返回对象,则返回新创建的对象。
通过构造函数new关键字生成一个实例对象,此时this指向这个实例对象。
js
function test() {
this.x = 1;
}
var obj = new test();
obj.x // 1
在上面的代码中,new关键字改变了this的指向。
我们来看一些特殊情况:
new过程遇到return一个对象,此时this指向为返回的对象
js
`function fn()
{
this.user = 'xxx';
return {};
}
var a = new fn();
console.log(a.user); //undefined`
如果返回一个简单类型的时候,则this指向实例对象
js
`function fn()
{
this.user = 'xxx';
return 1;
}
var a = new fn;
console.log(a.user); //xxx`
要特别注意的是null虽然也是对象,但是此时new仍然指向实例对象
js
`function fn()
{
this.user = 'xxx';
return null;
}
var a = new fn;
console.log(a.user); //xxx`
显式绑定
apply()、call()、bind()是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后调用这个函数的对象。因此,这是this指的就是这第一个参数。
js
var x = 0;
function test() {
console.log(this.x);
}
var obj = {};
obj.x = 1;
obj.m = test;
obj.m.apply(obj) // 1
关于apply()、call()、bind()三者的区别,以及它们的用法,我后面会单独拿一篇文章来详细说明。
三、特殊的箭头函数
箭头函数不仅仅是编写简洁代码的捷径,它还具备非常特殊且有用的特性。
JavaScript中充满了需要我们在其他地方指向的小函数的情况。 例如
arr.forEach(func)------forEach对每个数组元素都执行func。setTimeout(func)------func由内建调度器执行。
JavaScript的精髓在于创建一个函数并将其传递到某个地方。在这样的函数中,我们通常不想离开上下文。这就是箭头函数发挥作用的时刻了。
箭头函数没有this
箭头函数没有this,如果访问this,则会从外部获取。
js
const obj = {
sayThis: () => {
console.log(this);
}
};
obj.sayThis(); // window 因为 JavaScript 没有块作用域,所以在定义 sayThis 的时候,里面的 this 就绑到 window 上去了
const globalSay = obj.sayThis;
globalSay(); // window 浏览器中的 global 对象
虽然箭头函数的this能够在编译的时候就确定了this的指向,但也需要注意一些潜在的坑。
绑定事件监听
js
const button = document.getElementById('mngb');
button.addEventListener('click', ()=> {
console.log(this === window) // true
this.innerHTML = 'clicked button'
})
我们其实是需要this为点击的button,但此时指向了window
包括在原型上添加方法 的时候,此时this指向window
js
`Cat.prototype.sayName = () => {
console.log(this === window) //true
return this.name
}
const cat = new Cat('mm');
cat.sayName()`
箭头函数不能作为构造函数
构造函数必须能够动态绑定this到新创建的对象上,而箭头函数的this是静态绑定的,无法动态改变,因此无法满足构造函数的需求。
箭头函数不能作为构造函数的原因具体有以下三点
(1)无法动态绑定this
构造函数的核心是通过new操作符将this绑定到新对象上。而箭头函数的this是定义是固定的,无法被new操作符覆盖或修改。例如:
js
const Foo = (name) => { this.name = name; // ❌ 报错:this不可用 };
const instance = new Foo("bar"); // TypeError: Foo is not a constructor
箭头函数内部的this始终指向定义时的上下文(如全局对象或外部函数的this),无法绑定到新创建的对象上。
(2)无法设置原型链
构造函数的prototype属性用于实现原型继承。而箭头函数没有prototype属性,因此它们不能作为构造函数使用。例如:
js
const Bar = () => {};
console.log(Bar.prototype); // undefined
(3)new操作符的限制
new操作符要求目标函数必须具备[[Construct]]内部方法(即可以被用作构造函数)。而箭头函数内部没有[[Construct]]方法,因此尝试使用new调用箭头函数会直接抛出错误:
js
const Baz = () => {};
new Baz(); // TypeError: Baz is not a constructor
四、this绑定的优先级
隐式绑定 VS 显式绑定
js
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2
显然,显式绑定的优先级更高
new绑定 VS 隐式绑定
js
function foo(something) {
this.a = something;
}
var obj1 = {
foo: foo
};
var obj2 = {};
obj1.foo( 2 );
console.log( obj1.a ); // 2
obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
可以看到,new绑定的优先级>隐式绑定
new绑定 VS 显式绑定
因为new和apply、call无法一起使用,但硬绑定也是显式绑定的一种,可以替换测试
js
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar( 3 );
console.log( obj1.a ); // 2
console.log( baz.a ); // 3
bar被绑定到obj1上,但是new bar(3) 并没有像我们预计的那样把obj1.a修改为3。但是,new修改了绑定调用bar()中的this
我们可认为new绑定优先级>显式绑定
综上,new绑定优先级 > 显示绑定优先级 > 隐式绑定优先级 > 默认绑定优先级