在写程序时,我们都知道this很好用,但是却很容易导致乱用。就像我刚开始学习箭头函数时,我知道这个箭头指代的是this,但是却不知道它往哪里指,所以在写程序时,就会想当然的乱写,导致有时候因为一个数据获取不到而疯狂找错,这无形之中要增加很大的时间成本,不懂原理胡来总是很容易事后两行泪(T_T)
在下面的这边文章中,将讲解关于this的几大应用场景以及了解在面试中经常会被问到的apply、bind和call究竟是什么,接下来开始进入本文的讲解。
一、谈谈对this对象的理解
this
,函数执行的上下文,总是指向函数的直接调用者(而非间接调用者),可以通过apply
,call
,bind
改变this
的指向。如果有
new
关键字,this
指向new
出来的那个对象。在事件中,
this
指向触发这个事件的对象,特殊的是,IE
中的attachEvent
中的this
总是指向全局对象window
。对于匿名函数或者直接调用的函数来说,
this
指向全局上下文(浏览器为window
,NodeJS
为global
),剩下的函数调用,那就是谁调用它,this
就指向谁。对于
es6
的箭头函数,箭头函数的指向取决于该箭头函数声明的位置,在哪里声明, this 就指向哪里。
二、this的应用场景
在程序中,this主要有以下5种应用场景:
- 作为普通函数被调用
- 使用
call
、apply
和bind
被调用 - 作为对象方法被调用
- 在
class
方法中被调用 - 箭头函数中被调用
1、作为普通函数被调用
当 this
作为普通函数被调用时,指向 window
全局。
function fn1(){
console.log(this);
}
fn1(); //window
2、使用call、apply和bind被调用
当 this
使用 call
、 apply
和 bind
被调用时,直接指向作用域内的内容。
function fn1(){
console.log(this);
}
fn1(); //window
fn1.call({
x : 100 }); //{x : 100}
fn1.apply({
x : 200}); //{x : 200}
const fn2 = fn1.bind({
x : 200 });
fn2(); //{ x : 200 }
3、作为对象方法被调用
从下面代码中可以得出,当 this
放在 sayHi()
方法里面时,此时作为 zhangsan
对象的方法被调用,指向的是当前的对象。而放在 wait()
方法时,里面还有一个定时器,定时器里面还有一个函数,所以第二个 this
是作为普通函数被调用,指向 window
全局。
const zhangsan = {
name: '张三',
sayHi(){
//this 即当前对象
console.log(this);
},
wait(){
setTimeout(function(){
//this === window
console.log(this);
});
}
}
4、在class方法中被调用
从以下代码中可以看出,当 this
在 class
中被调用时,指向的是整个对象。
class People{
constructor(name){
this.name = name;
this.age = 20;
}
sayHi(){
console.log(this);
}
}
const zhangsan = new People('张三');
zhangsan.sayHi(); //zhangsan 对象
5、箭头函数中被调用
看到以下代码,细心的小伙伴不难发现,跟我们上面第3点看到的似乎有点类似,主要区别在于定时器中的函数改为了箭头函数。当改为箭头函数时,此时的this指向的是zhangsan这一个整个对象,而不再是指向全局。
const zhangsan = {
name: '张三',
sayHi(){
//this 即当前对象
console.log(this);
},
waitAgain(){
setTimeout(() => {
//this 即当前对象
console.log(this);
});
}
}
讲完箭头函数,我们来梳理下箭头函数和普通函数的区别,以及箭头函数是否能当做是构造函数的问题。
(1)箭头函数和普通函数定义
普通函数通过 function
关键字定义, this
无法结合词法作用域使用,在运行时绑定,只取决于函数的调用方式,在哪里被调用,调用位置。(取决于调用者,和是否独立运行)
箭头函数使用被称为 “胖箭头” 的操作 => 定义,箭头函数不应用普通函数 this
绑定的四种规则,而是根据外层(函数或全局)的作用域来决定 this
,且箭头函数的绑定无法被修改( new
也不行)。
(2)箭头函数和普通函数的区别
- 箭头函数常用于回调函数中,包括事件处理器或定时器。
- 箭头函数和
var self = this
,都试图取代传统的 this 运行机制,将 this 的绑定拉回到词法作用域。 - 箭头函数没有原型、没有
this
、没有super
,没有arguments
,没有new.target
。 - 箭头函数不能通过
new
关键字调用。- 一个函数内部有两个方法:
[[Call]]
和[[Construct]]
,在通过new
进行函数调用时,会执行[[construct]]
方法,创建一个实例对象,然后再执行这个函数体,将函数的this
绑定在这个实例对象上。 - 当直接调用时,执行
[[Call]]
方法,直接执行函数体。 - 箭头函数没有
[[Construct]]
方法,不能被用作构造函数调用,当使用new
进行函数调用时会报错。
- 一个函数内部有两个方法:
function foo() {
return (a) => {
console.log(this.a);
}
}
var obj1 = {
a: 2
}
var obj2 = {
a: 3
}
let bar1 = foo.call(obj1); //2
let bar2 = bar.call(obj2);
console.log(bar2); //undefind
(3)this绑定的四大规则
this绑定四大规则遵循以下顺序:
New 绑定 > 显示绑定 > 隐式绑定 > 默认绑定
下面一一介绍四大规则。
- 默认绑定:没有其他修饰(
bind
、apply
、call
),在非严格模式下定义指向全局对象,在严格模式下定义指向undefined
。
function foo() {
console.log(this.a);
}
var a = 2;
foo(); //undefined
- 隐式绑定:调用位置是否有上下文对象,或者是否被某个对象拥有或者包含,那么隐式绑定规则会把函数调用中的
this
绑定到这个上下文对象。而且,对象属性链只有上一层或者最后一层在调用位置中起作用。
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo,
}
obj.foo(); // 2
- 显式绑定:通过在函数上运行
call
和apply
,来显式的绑定this
。
function foo() {
console.log(this.a);
}
var obj = {
a: 2
};
foo.call(obj); //2
显示绑定之硬绑定
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
function bind(fn, obj) {
return function() {
return fn.apply(obj, arguments);
};
}
var obj = {
a: 2
}
var bar = bind(foo, obj);
console.log(bar); //f()
- New 绑定:
new
调用函数会创建一个全新的对象,并将这个对象绑定到函数调用的this
。New
绑定时,如果是new
一个硬绑定函数,那么会用new
新建的对象替换这个硬绑定this
。
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); //2
三、apply、call和bind
1、apply、call和bind的共同用法
先说下三者的共同用法,三者的共同用法就是可以改变函数的this指向,并将函数绑定到上下文中。接下来讲述一个应用场景加深理解:
let obj1 = {
hobby: 'running',
add(favorite){
console.log(`在我的业余时间里,我喜欢${
favorite},但同时我也喜欢${
this.hobby}`);
}
}
let obj2 = {
hobby: 'learning'
}
obj1.add('reading'); //在我的业余时间里,我喜欢reading,但同时我也喜欢running
可以看到在最后一行代码中,我们调用了 obj1
中的 add
函数,并传入了一个参数 reading
。 add
函数中的 this
指的是他所在的对象 obj1
,所以 this.hobby
就是 running
, 但是我们如果想获得 obj2
中的hobby
, 又该怎么处理呢?这就涉及到我们平常所听到的 apply
、 call
和 bind
。
接下来开始讲解 apply
、 call
和 bind
。
2、apply
(1)语法: Array.prototype.apply(this, [args1, args2])
。
(2)传入参数:
第一个参数:传入 this
需要指向的对象,即函数中的 this
指向谁,就传谁进来;
第二个参数:传入一个数组,数组中包含了函数需要的实参。
(3)apply的作用:①调用函数;②指定函数中 this
的指向。
(4)代码演示:
/**
*
* @description 实现apply函数,在函数原型上封装myApply函数, 实现和原生apply函数一样的效果
*/
Function.prototype.myApply = function(context){
// 存储要转移的目标对象
_this = context ? Object(context) : window;
// 在转移this的对象上设定一个独一无二的属性,并将函数赋值给它
let key = Symbol('key');
_this[key] = this;
// 将数组里存储的参数拆分开,作为参数调用函数
let res = arguments[1] ? _this[key](...arguments[1]) : _this[key]();
// 删除
delete _this[key];
// 返回函数返回值
return res;
}
(5)前情回顾
实现了 myApply
之后,我们继续引用刚开始关于爱好的那个例子,来修改 this
的指向。
let obj1 = {
hobby: 'running',
add(...favorite){
//...favorite意味着可以接收多个参数
console.log(`在我的业余时间里,我喜欢${
favorite},但同时我也喜欢${
this.hobby}`);
}
}
let obj2 = {
hobby: 'learning'
}
obj1.add.myApply(obj2, ['reading', 'working']);
// 输出结果:在我的业余时间里,我喜欢reading,working,但同时我也喜欢learning
在 obj1.add.myApply(obj2, ['reading', 'working'])
这一行代码, 第一个参数将 obj1
中的 add
函数的 this
指向了 obj2
, 第二个参数以数组形式传入多个参数,作为 obj1
中的 add
函数传入的参数, 所以最后能将 reading
和 working
都输出。
3、call
(1)语法: Array.prototype.call(this, args1, args2)
(2)传入参数:
第一个参数:传入 this
需要指向的对象,即函数中的 this
指向谁,就传谁进来;
其余参数: 除了第一个参数,其他的参数需要传入几个,就一个一个传递进来即可。
(3)call的作用:①调用函数;②指定函数中 this
的指向。
(4)代码演示:
/**
*
* @description 实现apply函数,在函数原型上封装myApply函数, 实现和原生apply函数一样的效果
*/
Function.prototype.myCall = function(context){
// 存储要转移的目标对象
let _this = context ? Object(context) : window;
// 在转移this的对象上设定一个独一无二的属性,并将函数赋值给它
let key = Symbol('key');
_this[key] = this;
// 创建空数组,存储多个传入参数
let args = [];
// 将所有传入的参数添加到新数组中
for(let i =1; i < arguments.length; i++){
args.push(arguments[i]);
}
// 将新数组拆开作为多个参数传入,并调用函数
let res = _this[key](...args);
// 删除
delete _this[key];
// 返回函数返回值
return res;
}
(5)前情回顾
实现了 myCall
之后,我们继续引用刚开始关于爱好的那个例子,来修改 this
的指向。
let obj1 = {
hobby: 'running',
add(...favorite){
//...favorite意味着可以接收多个参数
console.log(`在我的业余时间里,我喜欢${
favorite},但同时我也喜欢${
this.hobby}`);
}
}
let obj2 = {
hobby: 'learning'
}
obj1.add.myCall(obj2, 'reading', 'working');
// 输出结果:在我的业余时间里,我喜欢reading,working,但同时我也喜欢learning
在 obj1.add.myCall(obj2, 'reading', 'working')
这一行代码, 第一个参数将 obj1
中的 add
函数的 this
指向了 obj2
, 第二个参数通过依次传入多个参数的形式,作为 obj1
中的 add
函数传入的参数, 所以最后能将 reading
和 working
都输出。
讲到这里,我们来梳理下 call
和 apply
的区别:
call
和 apply
唯一的区别就是在于给函数传入参数的形式不同, call
是将多个参数逐个传入, 而apply
是 将多个参数放在一个数组中,一起传入。
4、bind
(1)语法: Array.prototype.bind(this, args1, args2)
。
(2)传入参数:
第一个参数:传入 this
需要指向的对象,即函数中的 this
指向谁,就传谁进来;
其余参数: 除了第一个参数,其他参数的传递可以像 apply
一样的数组类型,也可以像 call
一样的逐个传入;但需注意的是后面需要加个小括号进行其余参数的传递。
(3)call的作用:①克隆当前函数,返回克隆出来的新函数;②新克隆出来的函数,该函数的this被指定了。
(4)代码演示:
/**
* @description 实现Bind函数,在函数原型上封装myBind函数 , 实现和原生bind函数一样的效果
*
*/
Function.prototype.myBind = function(context){
// 存储要转移的目标对象
let _this = context ? Object(context) : window;
// 在转移this的对象上设定一个独一无二的属性,并将函数赋值给它
let key = Symbol('key');
_this[key] = this;
// 创建函数闭包
return function(){
// 将所有参数先拆分开,再添加到新数组中,以此来支持多参数传入以及数组参数传入的需求
let args = [].concat(...arguments);
// 调用函数
let res = _this[key](...args);
// 删除
delete _this[key];
// 返回函数返回值
return res;
}
}
(5)前情回顾
实现了 myBind
之后,我们继续引用刚开始关于爱好的那个例子,来修改 this
的指向。
let obj1 = {
hobby: 'running',
add(...favorite){
//...favorite意味着可以接收多个参数
console.log(`在我的业余时间里,我喜欢${
favorite},但同时我也喜欢${
this.hobby}`);
}
}
let obj2 = {
hobby: 'learning'
}
obj1.add.myBind(obj2)(['reading', 'working']);
// 输出结果:在我的业余时间里,我喜欢reading,working,但同时我也喜欢learning
通过以上我们可以看到, bind
有点类似 apply
和 call
的结合,只不过它返回的是一个函数,需要自身再进行一次调用, 而传给这个函数的参数形式有两种方式,可以是像 apply
一样的数组形式, 也可以是像 call
一样的逐个传入的形式。
大家不要觉得这个后面加个小括号太麻烦,这就是 bind
的强大之处,有时候 bind
也会经常运用在函数柯里化中。
讲到这里,关于this的相关知识就讲完啦!接下来我们来做个总结。
5、做个小结
this
取什么样的值,是在函数执行时确定的,不是在函数定义的时候确定的。apply
、call
、bind
三者都是函数的方法,都可以改变函数的this
指向。apply
和call
都是改变函数this
指向,并传入参数后立即调用执行该函数。bind
是在改变函数this
指向后,并传入参数后返回一个新的函数,不会立即调用执行。apply
传入的参数是数组形式的,call
传入的参数是按顺序的逐个传入并以逗号隔开,bind
传入的参数既可以是数组形式,也可以是按顺序逐个传入。
四、写在最后
关于 this
的指向问题在前端的面试中尤为常见,大家可以按照上文中的顺序把 this
的知识点串联起来一起理解!同时,本文内容为本人理解所整理,可能会存在边界歧义等问题。如果有不理解或者有误的地方欢迎私聊我或加我微信指正~
- 公众号:星期一研究室
- 微信:MondayLaboratory
如果这篇文章对你有用,记得点个赞再走哦~