JavaScript 中的 this 关键字是一个非常重要的语法点,在开发过程中,我们会经常用到它。
学习 this 之前,我们先思考一个问题:JavaScript 中为什么会有 this ?
一、为什么要用 this?
1、一个通俗的例子
我们看一段话:
从前有座山
山上有座庙被叫作神庙
神庙里有很多小和尚
小和尚们非常喜欢这座神庙
所以小和尚们都经常挑水到神庙
读完这段话我们发现了一些问题:
- 这段话里出现了很多次神庙
- 不符合我们说话和描述的习惯
- 词语很累赘
所以,我们经常会这样干:
从前有座山
山上有座庙被叫作神庙
这里有很多小和尚
小和尚们非常喜欢这里
所以小和尚们都经常挑水到这里
只在说话的开始,保留第一个名词,后面重复用到时,都用这来代替。现在读起来就顺口多了,其实写代码也是一样的。
现在我们把上面的例子转化为代码语言。
2、用代码语言来描述
假设我们有个叫作 person 的对象:
const person = {
name: 'Deepspace',
age: 23,
phone: '1234567890',
sayHi: function () {
// ...
},
sayBye: function () {
// ...
}
};
这个对象有 name 、age 和 phone 三个属性,有 sayHi 和 sayBye 两个方法。
现在的需求是:
- 调用
person.sayHi(...),打印:你好,我是Deepspace,今年 23 岁。 - 调用
person.sayBye(...),打印:再见,记得我叫Deepspace哦,想约我的话打电话给我,我的电话是 1234567890。
如何实现呢?我们先用比较 “笨” 一些的方法实现:
const person = {
name: 'Deepspace',
age: 23,
phone: '1234567890',
sayHi: function (name, age) {
console.log(`你好,我是 ${name},今年 ${age} 岁。`);
},
sayBye: function (name, phone) {
console.log(`再见,记得我叫 ${name} 哦,想约我的话打电话给我,我的电话是 ${phone}。`);
}
};
调用方式是:
person.sayHi(person.name, person.age);
person.sayBye(person.name, person.phone);
改进一:
const person = {
name: 'Deepspace',
age: 23,
phone: '1234567890',
sayHi: function (self) {
console.log(`你好,我是 ${self.name},今年 ${self.age} 岁。`);
},
sayBye: function (self) {
console.log(`再见,记得我叫 ${self.name} 哦,想约我的话打电话给我,我的电话是 ${self.phone}。`);
}
};
调用方式是:
person.sayHi(person);
person.sayBye(person);
这次改进,我们把 person 直接作为参数传递给 sayHi 和 sayBye 方法,稍微好了一些。
改进二:
为什么不把参数 person 直接干点,调用 sayHi() 或者 sayBye 就好呢?这样多直接啊。
那么问题来了:如果 person.sayHi() 没有实参,那函数是如何接收到 person 的呢?
有两种实现方案:
- 方法1:依然把第一个参数
self当作person,这样形参就会永远比实参多出一个self。 - 方法2:隐藏
self,然后用关键字this来访问self。
Python 选择了第一种实现方案,JavaScript 选择了第二种实现方案。
Python:
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def sit(self):
print(self.name.title() + " is now sitting")
def roll_over(self):
print(self.name.title() + " rolled over!")
my_dog = Dog('willie', 6)
# 访问属性
print("My dog's name is " + my_dog.name.title() + '.')
print("My dog " + str(my_dog.age) + " years old.")
# 调用方法
my_dog.sit()
my_dog.roll_over()
JavaScript:
const person = {
name: 'Deepspace',
age: 23,
phone: '1234567890',
sayHi: function () {
console.log(`你好,我是 ${this.name},今年 ${this.age} 岁。`);
},
sayBye: function () {
console.log(`再见,记得我叫 ${this.name} 哦,想约我的话打电话给我,我的电话是 ${this.phone}。`);
}
};
调用方式是:
person.sayHi();
person.sayBye();
这就是 JavaScript 中 this 的由来。
二、this 的绑定规则
JavaScript 中的 this 有四种绑定规则。
1、默认绑定
当一个函数没有明确的调用对象的时候,也就是单纯作为独立函数被调用的时候,将对函数的 this 使用默认绑定,绑定到全局的 window 对象。
function defaultBind() {
console.log(this === window);
};
defaultBind(); // true
注意:需要在浏览器环境执行,
node环境下会报错:ReferenceError: window is not defined。
所以,如果我们在全局环境初始化一个变量,在独立函数中可以使用 this 来获取到该全局变量:
var name = 'Deepspace';
function box() {
console.log(this.name);
};
box(); // Deepspace
这里额外提一下:
在
ES5中,顶层对象的属性和全局变量是等价的,var命令和function命令声明的全局变量,自然也是顶层对象。所以:var a = 12; function f(){}; console.log(window.a); // 12 console.log(window.f); // f(){}但
ES6规定,var命令和function命令声明的全局变量,依旧是顶层对象的属性,但let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。所以:let a = 1; const f = function () { return 1; }; console.log(window.a); // undefined console.log(window.f); // undefined在全局作用域中,用
let和const声明的全局变量并没有在全局对象中,只是一个块级作用域(Script)中(可以在浏览器中设置断点查看)。
所以,要想访问用
let和const声明的全局变量,就不可以使用window。所以在上面演示this的默认绑定的时候,使用var来初始化变量。
我们把上面的例子改一下:
function box() {
function innerBox() {
console.log(this === window);
}
innerBox();
};
box(); // true
函数 innerBox 在一个外部函数 box 里面声明且调用,它的 this 依然指向 window 。
所以,没有明确的调用对象的时候,将对函数的 this 使用默认绑定:绑定到全局的 window 对象。记住这一点就好。我们再改一下这个例子:
var obj = {
box: function () {
function innerBox() {
console.log(this === window);
}
innerBox();
}
};
obj.box(); // true
结果依旧是 true,这里其实也用到了默认绑定。
2、隐式绑定
当函数被一个对象“包含”的时候,我们称函数的 this 被隐式绑定到这个对象里面了,这时候,通过 this 可以直接访问所绑定的对象里面的其他属性。
每当我们调用一个对象的方法时,都会出现隐式绑定。
例如:
var name = 'globe name';
const obj = {
name: 'Deepspace',
sayHi: function () {
console.log(this.name);
}
};
obj.sayHi(); // Deepspace
我们再看另一段代码:
var name = 'globe name';
function sayHi() {
console.log(this.name);
}
const obj = {
name: 'Deepspace',
sayHi: sayHi
};
obj.sayHi(); // Deepspace
sayHi 函数依旧打印的是 Deepspace 。也就是说:sayHi 函数并不会因为它是被定义在 obj 对象的内部还是外部而有任何区别。 this 还是可以访问到 obj 对象中的 name 属性。
我们再看个例子:
const obj = {
name: 'Deepspace',
sayHi: function () {
console.log(this.name);
}
};
var name = 'globe name';
const hello = obj.sayHi;
hello(); // globe name
为什么 hello 会输出 globe name 呢?
首先,我们需要明确,this 的绑定是动态的。
当使用 this 的函数被包含在对象中时,这个函数对于对象来说是独立的,并不被这个对象所完全拥有。 它和在对象外部声明函数,然后在对象内部通过属性名称的方式取得函数的引用是完全等价的。定义在对象内部的函数只是“恰好可以被这个对象调用”而已,而不是“生来就是为这个对象所调用”的。
所以,当把 obj.sayHi 这个函数赋值给变量 hello 时,hello 这个函数引用就和 obj 这个对象完全没有关系了。此时的 this 指向的是 window 而不是 obj ,所以会输出 globe name 。
再看个复杂点的例子:
var name = 'globe name';
const obj = {
name: 'Deepspace',
sayHi: function () {
console.log(this.name);
}
};
function sayHello(fn) {
fn();
}
sayHello(obj.sayHi); // globe name
如果函数调用位置是在一串对象属性链中呢?
var obj = {
a: 1,
obj2: {
a: 2,
obj3: {
a: 3,
getA: function () {
console.log(this.a);
}
}
}
};
obj.obj2.obj3.getA(); // 输出3
这个时候,this 绑定的是最内层的对象,也就是离得最近的对象。
3、显示绑定
call
在上面的隐式绑定的例子中,当把 obj.sayHi 这个函数赋值给变量 hello 时,此时的 this 指向的是 window 而不是 obj ,所以会输出 globe name ,那如果一定要把 this 指向 obj 该怎么做呢?call 函数就派上用场了。
call 函数的语法:
fn.call(object);
fn 是你调用的函数,object 参数是希望函数的 this 所绑定的对象。
fn.call(object) 的作用:
-
立即调用这个函数
fn -
调用这个函数的时,将函数的
this指向object对象
看个例子:
const obj = {
name: 'Deepspace',
sayHi: function () {
console.log(this.name);
}
};
var name = 'globe name';
const hello = obj.sayHi;
hello(); // globe name
hello.call(obj); // Deepspace
现在问题来了:每次都写 call 去绑定 this 很麻烦,有没有一劳永逸的方式呢?当然有,我们可以这样做:
const obj = {
name: 'Deepspace',
sayHi: function () {
console.log(this.name);
}
};
var name = 'globe name';
const hello = obj.sayHi;
hello(); // globe name
const greet = function () {
hello.call(obj); //硬绑定
};
hello.call(obj); // Deepspace
greet(); // Deepspace
在 hello.call(obj) 的外边在包一层函数就可以了,这种方式叫做硬绑定。
bind
也可以把 bind 方法来简化这种写法:
const obj = {
name: 'Deepspace',
sayHi: function () {
console.log(this.name);
}
};
var name = 'globe name';
const hello = obj.sayHi;
hello(); // globe name
const regard = hello.bind(obj); // 使用 bind 绑定
regard(); // Deepspace
从例子可以看出:call 和 bind 的区别在于,在绑定 this 到对象参数的同时,call 将立即执行该函数,bind 不执行函数,只返回一个可供执行的函数。
apply
apply 方法的作用与 call 方法类似,也是改变 this 指向,然后再调用该函数。**唯一的区别就是,它接收一个数组作为函数执行时的参数。**看个例子:
function getAge(gap) {
var y = new Date().getFullYear();
return y - this.birth - gap;
}
var chenxingxing = {
name: '陈星星',
birth: 1995,
age: getAge
};
console.log(chenxingxing.age(1)); // 23
console.log(getAge.apply(chenxingxing, [1])); // 23, this 指向 chenxingxing, 参数为空
4、new 绑定
执行 new 操作符来调用函数的时候,将创建一个新的对象,并且将构造函数的 this 指向所创建的新对象。如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象:
function foo(a) {
this.a = a;
}
const a1 = new foo(1);
const a2 = new foo(2);
const a3 = new foo(3);
const a4 = new foo(4);
console.log(a1.a); // 1
console.log(a2.a); // 2
console.log(a3.a); // 3
console.log(a4.a); // 4
但是如果函数中返回的有其他对象,此时的 this 绑定在其返回的对象上:
function foo(a) {
this.a = a;
return {
m: 99
};
}
const a1 = new foo(1);
const a2 = new foo(2);
console.log(a1.a); // undefined
console.log(a2.a); // undefined
console.log(a2.m); // 99
三、不好的实践
由于 this 的指向是不确定的,所以使用起来尤其有注意。有几个不好的实践需要避免:
1、避免在函数中包含多层的 this
看个函数:
var o = {
f1: function () {
console.log(this);
function temp() {
console.log(this);
}
return temp;
}
};
var c = o.f1();
c();
// Object: { f1: ƒ }
// Window
上面的函数包含了两层 this ,第一层指向对象 o ,第二层指向 Window。实际上的执行代码是:
function temp() {
console.log(this);
}
var o = {
f1: function () {
console.log(this);
return temp;
}
};
var c = o.f1();
c();
// Object: { f1: ƒ }
// Window
很容易弄混淆。所以我们常用的做法是:
var o = {
f1: function () {
console.log(this);
const that = this;
function temp() {
console.log(that);
}
return temp;
}
};
var c = o.f1();
c();
// Object: { f1: ƒ }
// Object: { f1: ƒ }
在第二层改用一个指向外层 this 的变量。
同时,JavaScript 提供了严格模式,也可以硬性避免这种问题。
严格模式下,当函数内部的 this 指向全局的 Window 时,会将 this 指向转为 undefined:
function a() {
function b() {
console.log(this);
function c() {
'use strict';
console.log(this);
}
c();
}
b();
}
a();
// Window
// undefined
2、避免数组处理方法中的 this
数组的 map 和 foreach 方法,允许提供一个函数作为参数。这个函数内部不应该使用 this。
var v = 'global v';
const o = {
v: 'hello',
p: ['a1', 'a2'],
f: function f() {
this.p.forEach(function (item) {
console.log(this.v + ' ----- ' + item);
});
}
};
o.f();
// global v ----- a1
// global v ----- a2
上面代码中,foreach 方法的回调函数中的 this,其实是指向 window 对象,因此取不到 o.v 的值。原因跟上一段的多层 this 是一样的,就是内层的 this 不指向外部,而指向顶层对象。
解决这个问题的一种方法,就是前面提到的,使用中间变量固定 this:
var v = 'global v';
const o = {
v: 'hello',
p: ['a1', 'a2'],
f: function f() {
const that = this;
this.p.forEach(function (item) {
console.log(that.v + ' ---- ' + item);
});
}
};
o.f();
// hello a1
// hello a2
3、避免回调函数中的 this
回调函数中的 this 往往会改变指向,最好避免使用。例如:
var o = new Object();
o.f = function () {
console.log(this === o);
};
// jQuery 的写法
$('#button').on('click', o.f); // false
上面代码中,点击按钮以后,控制台会显示 false 。原因是此时 this 不再指向 o 对象,而是指向按钮的 DOM 对象,因为 f 方法是在按钮对象的环境中被调用的。
四、最后
最后来演练一下 this 的指向吧:
var name = '小红';
function a() {
const name = '小白';
console.log(this.name);
}
function d(i) {
return i();
}
const b = {
name: '小黄',
detail: function () {
console.log(this.name);
},
hello: function () {
return function () {
console.log(this.name);
};
}
};
var nickname = 'The Window';
var object = {
nickname: 'My Object',
getNameFunc: function () {
var that = this;
return function () {
return that.nickname;
};
}
};
const c = b.detail;
b.a = a;
const e = b.hello();
a(); // 小红
c(); // 小红
b.a(); // 小黄
d(b.detail); // 小红
e(); // 小红
const f = object.getNameFunc;
f()(); // The Window
object.getNameFunc()(); // 'My Object'
来源:CSDN
作者:IDeepspace
链接:https://blog.csdn.net/Deepspacece/article/details/104338699