前言
前端开发的小伙伴多多少少曾被 this 关键字难倒过,因为 JS 的 this 的指向很多时候可以是“动态变化”的,但是关于
this 关键字我们只需要记住一点:哪个对象调用函数,函数的this指向哪个对象。
但是这个判断是谁就是一个不那么简单的过程了,接下来我们就一一举例说明。
1、普通函数调用
1.1、基本调用
像这样一个直接声明的函数,它的this指向谁呢?
function outer(){
console.log(this);
function inner(){
console.log(this);
}
inner();
}
outer();
// Window
// Window
上面这个例子说明直接声明在作用域里的函数this都指向Window对象。
但是真的是这样吗?接下来我们看看这两个例子。
1.2、严格模式
// 注: 'use strict' 只能声明在作用域的第一行
'use strict';
function outer0(){
console.log(this);
function inner0(){
console.log(this);
}
inner0();
}
outer0();
// undefined
// undefined
outer0.call(0);
// 这里会输出什么?
function outer1(){
console.log(this);
function inner1(){
'use strict';
console.log(this);
}
inner1();
}
outer1();
// Window
// undefined
outer1.call(0);
// 这里又会输出什么?
所以,我们得出:直接声明在作用域的函数,在非严格模式下其this指向Window对象;在严格模式下指向undefined。 这一点并不会随父作用域的this变化而变化。
小伙伴们可以自行到浏览器看看通过call(thisValue)调用输出内容,关于这点,稍后我们也会讲到。
接下来我们所有代码都默认在严格模式下运行
2、对象调用
2.1、基本调用
'use strict';
var key = 20;
var value = 20;
var obj = {
key: 10,
fn: function(){
console.log(this.key);
console.log(this.value);
}
};
obj.fn();
// 10
// undefined
我们可以看到,输出的this.key是10,而不是20;而对象上没有value属性,输出的this.value得到的是undefined,并不会因为外面声明了一个value = 20就输出20,故我们可以得出,通过对象调用函数,其this指向当前对象。
2.2、相同函数,由不同对象调用
但是下面这种情况应该注意:
'use strict';
var obj1 = {
key: 10,
fn: function(){
console.log(this.key);
console.log(this.value);
},
};
var obj2 = {
key: 20,
fn: obj1.fn,
}
obj1.fn();
// 10
// undefined
obj2.fn();
// 20
// undefined
虽然obj2.fn等于obj1.fn,但调用它的是obj2,所以函数this指向是obj2,哪个对象调用函数,函数里面的this指向哪个对象。
2.3、如果我们把 fn 拿出来会怎样呢?
// 这里不能用严格模式了,有兴趣的可以看看使用严格模式会怎样,以及为什么~~
// 'use strict';
var obj = {
key: 10,
fn: function(){
console.log(this.key);
console.log(this.value);
},
};
var fn = obj.fn;
obj.fn();
// 10
// undefined
fn();
// undefined
// undefined
我们可以看见,当我们拿出来单独调用时,它输出了两个undefined,这是因为:哪个对象调用函数,函数里面的this指向哪个对象。当我们拿出来后,实际上是由Window对象调用,进一步证明了:哪个对象调用函数,函数里面的this指向哪个对象。同时也说明了,一个声明的普通函数调用,都是由全局对象Window调用,严格模式下是由undefined调用。(undefined居然能调用方法?~~~假装吧,但结果很重要)
3、通过构造函数调用
3.1、通常情况:构造函数没有return语句
'use strict';
function Student(name){
this.name = name;
// 当通过 new 调用时,这可认为有一条隐式的语句
// return this;
}
var student = new Student('lilei');
console.log(student.name);
// lilei
我们都知道是这样一个结果,但是为什么呢?这我们就需要理清当我们 new一个对象的时候,js 都帮我们干了什么呢,其实很简单:
// 创建一个空对象
var obj = {};
// 将新创建的对象 __proto__ 指向 Student 的 prototype
obj.__proto__ = Student.prototype;
// 将新创建对象的指针指向 Student 函数
Student.call(obj);
当经过这样一个步骤之后,当我们执行new Student('lilei')时,我们就知道它的this指向为什么是这么个结果了。
等等,上面代码提到了通过new调用时,有一个隐式return this;语句,那如果我们显示的写出return语句会如何呢?
3.1、当构造函数包含return语句时
'use strict';
function Student(name){
this.name = name;
return {
name: 'benshaoye',
}
}
var student = new Student('lilei');
console.log(student.name);
// benshaoye
纳尼,这是为什么?当我们在构造函数里有return时,new出来的对象就是return的对象,而不是Student对象,需要注意,不过这种情况也挺少(我就从来没遇见过)。
4、通过 call、apply、bind 调用
在上面的例子中我们已经使用过call了,apply也有类似的地方,那么它们的作用是什么,区别又是什么呢?
call、apply和bind的第一个参数始终是函数执行时this指向的对象。
4.1 Function.prototype.call( thisValue, ...args)
call后面其他的参数是执行函数传给函数的参数:
'use strict';
function sum(a, b){
return this + a + b;
}
/*
* 这里 this 指向 1,参数 a、b 分别是 2、3
*/
console.log(sum.call(1, 2, 3));
// 6
4.2 Function.prototype.apply( thisValue, [...args])
apply只接受两个参数,第一个参数是执行时this指向的对象,第二个参数是一个数组,数组里的参数就是函数执行时参数,通过例子说明:
'use strict';
function sum(a, b){
return this + a + b;
}
/*
* 这里 this 指向 1,参数 a、b 分别是 2、3
* 通过例子更能理解其中的差异
*/
console.log(sum.apply(1, [2, 3]));
// 6
var obj = {
key: 123,
fn: function(){
console.log(this.key);
},
fn0: ()=> {
console.log(this)
}
}
obj.fn();
// 123
// 这里虽然调用的是 obj.fn 方法
// 但是通过 call 动态改变了它的 this 指向了 target
// 此时相当于 obj.fn 的执行环境是 target
var target = {key: 456}
obj.fn.apply(target);
// 456
// 稍后会讲到,箭头函数不能改变其 this 指向
obj.fn0.apply(target);
// undefined
4.3 Function.prototype.bind( thisValue, ...args)
这里需要注意,bind的第一个参数thisValue是this指向的对象,后面的参数依次是要传给函数的参数,并且不可修改,返回的是另外一个函数,这个函数接受的参数是剩余的参数,举例说明:
'use strict';
function sum(a, b){
return this + a + b;
}
var otherSum = sum.bind(1, 2);
console.log(otherSun(3));
// 6
可以看出,this绑定了1,a被绑定了2,执行是3,就是b,很高级。
有兴趣的小伙伴可以先了解下 柯里化,以后也会讲到。
4.4 如何自己实现call、apply和bind?
这个问题有时候面试经常会问到,就贴出来一下,供参考。
Function.prototype.call0 = function(){
var args = arguments, argsLen = args.length;
var self = this;
var customKey = 'customKey';
// 或者 customKey = Symbol.for('fn'),这样更安全
var thisValue = args[0];
thisValue[customKey] = self;
var paramsName = [];
for (var i = 0; i < argsLen; i++){
params.push('args[' + i + ']');
}
// 这里还有另外的方法,如:
// self.apply(thisValue, params),不过我们就是来实现这两个的,是不是有点换汤不换药的味道
// thisValue[customKey](...params), 解构语法
var returnVal = eval('thisValue[customKey](' + paramsName.join(',') +')');
// 删除我们添加的属性
delete thisValue[customKey];
return returnVal;
}
// 二者实现很相似
Function.prototype.apply0 = function(){
var args = arguments, argsLen = args.length;
var self = this;
var customKey = 'customKey';
// 或者 customKey = Symbol.for('fn'),这样更安全
var thisValue = args[0];
var params = args[1];
thisValue[customKey] = self;
var paramsName = [];
for (var i = 0; i < argsLen; i++){
params.push('params[' + i + ']');
}
// 其实这里最好也用解构语法,这种方式很烦
var returnVal = eval('thisValue[customKey](' + paramsName.join(',') +')');
// 删除我们添加的属性
delete thisValue[customKey];
return returnVal;
}
// 以后直接用解构语法这些新特性了,这才是明智的选择!!!
Function.prototype.bind0 = function(thisValue, ...args){
const thisLen = this.length, argsLen = args.length;
const self = this;
if (thisLen < argsLen) {
const params = args.slice(1, thisLen);
return function(){
return self.apply(thisValue, params)
}
} else {
return function(...otherArgs){
return self.apply(thisValue, args.concat(otherArgs));
}
}
}
5、箭头函数
ES6 提供的箭头函数,大大增加了我们的开发效率,但是在箭头函数里面,是没有this上下文的,箭头函数里的this是继承外面的环境。
'use strict';
const obj1 = {
value: 123,
fn: function(){
setTimeout(function(){
// 这里的 function 是直接声明
// 在调用的时候是由全局对象调用
console.log('this.value', this.value);
});
}
}
const obj2 = {
key: 456,
fn: function(){
// 如果说普通函数式动态绑定 this 上下文
// 箭头函数的 this 上下文则是静态绑定
// 始终指向 obj2
const callback = () => {
console.log('this.key', this.key);
}
setTimeout(callback);
return callback;
}
}
obj1.fn();
const obj2Arrows = obj2.fn();
obj2Arrows.call({key: 789});
// 考虑一下事件循环,执行 setTimeout 不会马上输出,而是在下一个循环输出
// 如有不明白的可以先自行查阅资料,以后也会讲到这。
// 顺序不一定
// this.value undefined
// this.key 456
// this.key 456
可以看出,obj2Arrows通过call 执行,并不能改变其this指向,而是始终指向obj2,印证了箭头函数this指向是静态编译,始终指向与其父作用域的this相同的论点。看下面例子:
function handler() {
const fn = ()=> {
console.log('this.value0', this.value)
};
fn();
console.log('this.value1', this.value)
}
handler.call({value: 1});
handler.call({value: 2});
// this.value0 1
// this.value1 1
// this.value0 2
// this.value1 2
小结
- 直接声明的函数直接调用时,
this指向全局对象Window,严格模式下指向undefined; - 普通函数的
this始终指向调用它的那个对象; - 通过
call、apply、bind调用时,this指向其第一个参数; - 箭头函数的
this是静态编译,始终与其父作用域的this一致;
谢谢大家,喜欢点个赞再走哦!!!