前言
在学习JavaScript的过程中,this是一个不可忽视的概念,很多巧妙的操作都是基于this实现的。但this在不同环境下的取值往往是一个令人头疼的问题。
this是如何定义的
先看一下官方文档中对this的说明。
ECMAScript标准中this的解释
The abstract operation ResolveThisBinding determines the binding of the keyword this using the LexicalEnvironment of the running execution context
大致意思是,this关键字是一个运行时的语义,指向活跃状态的执行上下文的词法环境(LexicalEnvironment)。
MDN中this的定义
当前执行上下文(global、function 或 eval)的一个属性,在非严格模式下,总是指向一个对象,在严格模式下可以是任意值。
小结
上面的两种关于this的解释都和上下文有关,在JavaScript中,一段代码在执行前 会生成一个活动对象,作为代码的执行环境,里面存储了代码段中声明的变量,函数等,我们也将活动对象称做执行上下文。
除了变量,函数外,在执行上下文中还会确定this的值,也就是this绑定。在this绑定过程中,this一般指向代码运行时所在的环境(LexicalEnvironment)。代码执行过程中获取的this都是从执行上下文中取值的。
this有什么作用
万物皆有因,在JavaScript这门编程语言中this被创建出来一定有存在的意义,能够发挥一定的作用。
this设计的目的就是在函数体内部,指代当前函数的运行环境。因此我们可以通过this在函数体内部便捷的引用运行环境中的变量而不需要传参,极大的方便了开发。
我们通过一个简单的例子来体验一下this的效果。
javascript
var person = {
name: '小明',
age: 18,
hobby: '篮球',
playGame: function(){
// 待补充
}
}
现在有一个对象person,有name,age,hobby三个属性,还有一个playGame方法,现在我们的需求是:
调用person.playGame()方法,输出:我叫小明,今年18岁,我的爱好是篮球!
如果没有this,我们该如何实现这个操作?直接的思路就是传入需要的数据,然后输出。
javascript
var person = {
name: '小明',
age: 18,
hobby: '篮球',
playGame: function(name,age,hobby){
console.log(`我叫${name},今年${age}岁,我的爱好是${hobby}!`)
}
}
person.playGame(person.name,person.age,person.hobby);
再简化一下,直接传入person对象,得到的代码为:
javascript
var person = {
name: '小明',
age: 18,
hobby: '篮球',
playGame: function(self){
console.log(`我叫${self.name},今年${self.age}岁,我的爱好是${self.hobby}!`)
}
}
person.playGame(person);
如果是使用this来实现这个需求,则代码为:
javascript
var person = {
name: '小明',
age: 18,
hobby: '篮球',
playGame: function(){
console.log(`我叫${this.name},今年${this.age}岁,我的爱好是${this.hobby}!`)
}
}
person.playGame();
两者对比发现,区别不是很大,只不过person是显示传参,需要我们手动设置,存储在函数的参数列表里面 。
而this是隐式的,JavsScript直接帮我们设置了,存储在代码执行前生成的上下文中。通过this,我们可以在函数内部获取当前函数的运行环境,使用环境中的一些变量。这也是this最初被设计的目的。
再看一个经典的例子。
javascript
var f = function () {
console.log(this.x);
}
var x = 1;
var obj = {
f: f,
x: 2,
};
// 单独执行
f() // 1
// obj 环境执行
obj.f() // 2
同一个函数,在不同的环境中执行,它的this指向是不同的,它总是指向当前函数的运行环境。
判断this的指向
this一般在函数中使用,但是在不同的函数中,this指向也是有所区别的。
标准函数
在标准函数中,this引用的是把函数当成方法调用的上下文对象,这时通常称其为this值。
在全局上下文中调用函数时,this指向window
javascript
var foo = 123;
function print(){
this.foo = 234;
console.log(this); // window
console.log(foo); // 234
}
print(); //等价于 window.print()
在全局上下文中调用,等价于window对象在调用这个函数,此时函数的this指向的是window对象。
以某个对象的方法调用时,this指向这个对象
javascript
var color = 'red';
let o = {
color: 'blue',
};
function sayColor() {
console.log(this.color);
}
sayColor(); //'red'
o.sayColor = sayColor;
o.sayColor(); //'blue'
小结
其实上面两种情况,都可以看做是同一种情况,即把函数当做方法调用 。只不过在全局调用时,window对象是可以省略的。
从上面的例子可以看出,同一个函数在不同的地方调用,其this的指向是不同的 。也就是说,this的指向是在函数调用时 确定的,和函数在哪个作用域创建无关。
做几道例题测试下吧
1,let,const
javascript
let a = 1;
const b = 2;
var c = 3;
function print() {
console.log(this.a); //undefined
console.log(this.b); //undefined
console.log(this.c); //3
}
print();
console.log(this.a); //undefined
let/const定义的变量存在暂时性死区,而且不会挂载到window对象上,因此print中是无法获取到a和b的。
2,对象内执行
javascript
var a = 1;
function foo() {
console.log(this.a);
}
const obj = {
a: 10,
bar() {
foo(); // 1
}
}
obj.bar();
foo虽然在obj的bar函数中,但foo函数仍然是独立运行的,foo中的this依旧指向window对象。
3,函数内执行
javascript
var a = 1
function outer() {
var a = 2
function inner() {
console.log(this.a) // 1
console.log(a); //2
}
inner()
}
outer()
查找标准函数中某个变量的值时,可以通过作用域链寻找,但寻找标准函数中的this值时,需要判断它被哪个对象所调用。inner()正常执行,不是作为对象的某个方法调用,this指向Window
4,自执行函数
javascript
var a = 1;
(function(){
console.log(this);
console.log(this.a)
}())
function bar() {
b = 2;
(function(){
console.log(this);
console.log(this.b)
}())
}
bar();
默认情况下,自执行函数的this指向window 自执行函数只要执行到就会运行,并且只会运行一次,this指向window。
箭头函数
箭头函数的this
箭头函数没有this,它的this指向的是定义箭头函数的作用域中对应的上下文。
javascript
var color = 'red';
let o = {
color: 'blue',
};
let sayColor = () => console.log(this.color)
sayColor(); //'red'
o.sayColor = sayColor;
o.sayColor(); //'red'
上述代码中,箭头函数是在全局作用域中定义的,也就是说this会指向全局作用域。由于JS采用的是静态作用域,作用域 是在代码创建时 就确定了的,所以,箭头函数中的this是指向定义箭头函数的作用域中执行上下文的。
如果是在全局中定义箭头函数 ,箭头函数中的this指向全局上下文,全局上下文中的this是指向window。
如果是在标准函数中定义箭头函数 ,那么箭头函数的this指向标准函数上下文中的this。标准函数的this是根据函数执行的位置确定的。
做几道例题测试下
1 在对象方法中使用箭头函数
javascript
var name = 'tom'
const obj = {
name: 'zc',
intro: () => {
console.log('My name is ' + this.name)
}
}
obj.intro() //My name is tom
箭头函数在全局作用域中创建,this指向全局上下文。
2 箭头函数与普通函数比较
javascript
var name = 'tom';
const obj = {
name: 'zc',
intro: function () {
return () => {
console.log('My name is ' + this.name)
}
},
intro2: function () {
return function () {
console.log('My name is ' + this.name)
}
}
}
obj.intro2()() //My name is tom
obj.intro()() //My name is zc
分析:调用intro方法返回值是一个箭头函数,箭头函数中的this是取定义箭头函数作用域中的this,也就是intro函数的作用域。intro函数是一个标准函数,它的this根据调用位置确定,obj调用的intro方法,所以intro中的this指向obj。obj.intro()() //My name is zc
intro2方法返回值是一个标准函数,它的this是根据调用位置来确定的。其在全局中调用,所以this指向window
javascript
obj.intro2()() //My name is tom
//等价于
let fn = obj.intro2();
fn() //My name is tom 全局中调用
new 构造函数
构造函数中this的指向
如果函数是使用new调用,函数执行前会新创建一个对象,this指向这个创建的对象。
javascript
function User(name, age) {
this.name = name;
this.age = age;
}
var name = 'Tom';
var age = 18;
var zc = new User('zc', 24);
console.log(zc.name) // 'zc'
new 操作符实现的操作
- 创建一个对象。
- 将对象的原型对象设置为构造函数的原型对象,来继承原型对象上的属性和方法。
- 将构造函数中的this指向该对象,为这个对象添加属性和方法。
- 返回这个对象。
new 操作符手写实现
javascript
function createNew(con,...args){
//以构造函数原型对象为原型对象创建一个对象
let obj = Object.create(con.prototype);
//将构造函数中的this指向这个对象
const result = con.call(obj,...args);
// 返回创建的对象
return result instanceof Object? result:obj;
}
//测试
function Person(name,age){
this.name = name;
this.age = age;
}
let person1 = new Person('张三',25);
console.log(person1);
let person2 = createNew(Person,'李四',26);
console.log(person2);
控制台输出结果:
更改this的指向
函数中this的值是JavaScript代码在执行上下文中自动设置的,但JavaScript也提供了call
,apply
,bind
等方法让我们手动设置this的值。
call方法
方法说明
call方法使用一个指定的this值 和单独给出的一个或多个参数 来调用一个函数 。返回调用函数的返回值。
它将调用函数中this指向改成指向call方法中传入的第一个参数。
语法格式:function.call(thisArg, arg1, arg2, ...)
。
示例:
javascript
var name = "honny";
function sayHello(name,emotion){
console.log(`hello ${this.name},My name is ${name},I'm very ${emotion} to see you`);
}
//全局调用sayHello方法,this指向window
sayHello('Jock','happy');
//hello honny,My name is Jock,I'm very happy to see you
let person = {
name:'nack'
}
//使用call方法,将sayHello函数中的this指向person
sayHello.call(person,'Jock','happy');
//hello nack,My name is Jock,I'm very happy to see you
手写call方法
javascript
//手写call方法
function myCall(target,...args){
//获取目标对象
let obj = target||window;
//创建唯一标识
let symbolName = Symbol();
//获取方法:此时是需要执行的函数调用call方法,this指向执行的函数
//function.call() this->function
//在目标对象上添加这个函数
obj[symbolName] = this;
//执行目标对象上添加的函数 并获取返回值
//此次调用中,函数是在目标对象中调用的,所以函数中的this指向目标对象
let result = obj[symbolName](...args);
//调用完毕后删除目标对象上添加的函数
delete obj[symbolName];
//返回函数调用结果
return result;
}
//测试
var name = "honny";
function sayHello(name,emotion){
console.log(`hello ${this.name},My name is ${name},I'm very ${emotion} to see you`);
}
let person = {
name:'nack'
}
//在函数原型上添加myCall方法
Function.prototype.myCall = myCall;
//调用myCall方法
sayHello.myCall(person,'Jock','happy');
// hello nack,My name is Jock,I'm very happy to see you
apply方法
方法说明
apply调用一个具有给定值this的函数,以及以一个数组或类数组对象的形式提供的参数,返回调用函数的返回值。同call方法类似,它也能够将调用函数中的this的指向改变为apply方法传入的第一个参数。不过于call方法不同的是,call方法接收一个参数列表,但apply方法接收一个单数组。
语法格式:function.apply(thisArg,[...arg])
示例:
javascript
var name = "honny";
function sayHello(name,emotion){
console.log(`hello ${this.name},My name is ${name},I'm very ${emotion} to see you`);
}
sayHello('Jock','happy');
//hello honny,My name is Jock,I'm very happy to see you
let person = {
name:'nack'
}
sayHello.apply(person,['Jock','happy']);
//hello nack,My name is Jock,I'm very happy to see you
注意:apply以数组的方式接收调用函数的参数,但在调用函数中还是以参数列表的形式接收的。
手写apply方法
javascript
//手写apply
function myApply(target,argsArray){
//1 获取目标对象
let obj = target || window;
//2 创建唯一编码
let symbol = Symbol();
//3 在目标对象上添加函数
obj[symbol] = this;
//4 执行函数获取结果
let result = obj[symbol](...argsArray);
//5 执行完毕,删除函数
delete obj[symbol];
//6 返回执行结果
return result;
}
//测试
var name = "honny";
function sayHello(name,emotion){
console.log(`hello ${this.name},My name is ${name},I'm very ${emotion} to see you`);
}
let person = {
name:'nack'
}
Function.prototype.myApply = myApply;
sayHello.myApply(person,['Jock','happy']);
//hello nack,My name is Jock,I'm very happy to see you
bind方法
方法说明
bind方法创建一个新的函数,在bind被调用时,这个新函数的this值被指定为bind方法的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
与call和apply相同的是,bind方法能够将调用函数的this指向bind的第一个参数。但不同的是bind并不会执行这个函数,而是返回更改this后的新函数 。
语法格式:function.bind(thisArg, arg1, arg2, ...)
示例:
javascript
var name = "honny";
function sayHello(name,emotion){
console.log(`hello ${this.name},My name is ${name},I'm very ${emotion} to see you`);
}
sayHello('Jock','happy');
//hello honny,My name is Jock,I'm very happy to see you
let person = {
name:'nack'
}
//情况一
let sayHelloAfterBind = sayHello.bind(person,'Jock','happy');
sayHelloAfterBind();
//hello nack,My name is Jock,I'm very happy to see you
//情况二
let sayHelloAfterBind = sayHello.bind(person);
sayHelloAfterBind('Jock','happy');
//hello nack,My name is Jock,I'm very happy to see you
//情况三
let sayHelloAfterBind = sayHello.bind(person,'Jock');
sayHelloAfterBind('happy');
//执行方法时接收的实际参数为:'Jock','happy'
//hello nack,My name is Jock,I'm very happy to see you
注意:由于调用bind返回的是一个函数,函数是可以传参数的,函数执行使用的参数是bind第一个参数之后的参数加上调用函数时传入的参数。
手写bind方法
javascript
//手写 bind
function myBind(target,...args1){
//1 获取目标对象
let obj = target || window;
//2 创建唯一编码
const symbol = Symbol();
//3 再目标对象上添加函数
obj[symbol] = this;
//4 返回一个函数
return function(...args2){
return obj[symbol](...args1,...args2);
}
}
//测试
var name = "honny";
function sayHello(name,emotion){
console.log(`hello ${this.name},My name is ${name},I'm very ${emotion} to see you`);
}
let person = {
name:'nack'
}
Function.prototype.myBind = myBind;
let sayHelloMyBind = sayHello.myBind(person,'Jock');
sayHelloMyBind('happy');
//hello nack,My name is Jock,I'm very happy to see you
参考资料
JavaScript 的 this 原理 - 阮一峰的网络日志 (ruanyifeng.com)
JS 里为什么会有 this - 知乎 (zhihu.com)
ECMAScript® 2019 Language Specification (ecma-international.org)
面试官为啥总是让我们手撕call、apply、bind? - 掘金 (juejin.cn)