本文共 8815 字,大约阅读时间需要 29 分钟。
在写程序时,我们都知道this很好用,但是却很容易导致乱用。就像我刚开始学习箭头函数时,我知道这个箭头指代的是this,但是却不知道它往哪里指,所以在写程序时,就会想当然的乱写,导致有时候因为一个数据获取不到而疯狂找错,这无形之中要增加很大的时间成本,不懂原理胡来总是很容易事后两行泪(T_T)
在下面的这边文章中,将讲解关于this的几大应用场景以及了解在面试中经常会被问到的apply、bind和call究竟是什么,接下来开始进入本文的讲解。
this
,函数执行的上下文,总是指向函数的直接调用者(而非间接调用者),可以通过 apply
, call
, bind
改变 this
的指向。
如果有 new
关键字,this
指向 new
出来的那个对象。
在事件中,this
指向触发这个事件的对象,特殊的是,IE
中的 attachEvent
中的 this
总是指向全局对象 window
。
对于匿名函数或者直接调用的函数来说,this
指向全局上下文(浏览器为 window
,NodeJS
为 global
),剩下的函数调用,那就是谁调用它, this
就指向谁。
对于 es6
的箭头函数,箭头函数的指向取决于该箭头函数声明的位置,在哪里声明, this 就指向哪里。
在程序中,this主要有以下5种应用场景:
call
、 apply
和 bind
被调用class
方法中被调用当 this
作为普通函数被调用时,指向 window
全局。
function fn1(){ console.log(this);}fn1(); //window
当 this
使用 call
、 apply
和 bind
被调用时,直接指向作用域内的内容。
function fn1(){ console.log(this);}fn1(); //windowfn1.call({ x : 100 }); //{x : 100}fn1.apply({ x : 200}); //{x : 200}const fn2 = fn1.bind({ x : 200 });fn2(); //{ x : 200 }
从下面代码中可以得出,当 this
放在 sayHi()
方法里面时,此时作为 zhangsan
对象的方法被调用,指向的是当前的对象。而放在 wait()
方法时,里面还有一个定时器,定时器里面还有一个函数,所以第二个 this
是作为普通函数被调用,指向 window
全局。
const zhangsan = { name: '张三', sayHi(){ //this 即当前对象 console.log(this); }, wait(){ setTimeout(function(){ //this === window console.log(this); }); }}
从以下代码中可以看出,当 this
在 class
中被调用时,指向的是整个对象。
class People{ constructor(name){ this.name = name; this.age = 20; } sayHi(){ console.log(this); }}const zhangsan = new People('张三');zhangsan.sayHi(); //zhangsan 对象
看到以下代码,细心的小伙伴不难发现,跟我们上面第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); //2let 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
调用函数会创建一个全新的对象,并将这个对象绑定到函数调用的 this
。New
绑定时,如果是 new
一个硬绑定函数,那么会用 new
新建的对象替换这个硬绑定 this
。function foo(a) { this.a = a; }var bar = new foo(2); console.log(bar.a); //2
先说下三者的共同用法,三者的共同用法就是可以改变函数的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
。
(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
都输出。
(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
是 将多个参数放在一个数组中,一起传入。
(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的相关知识就讲完啦!接下来我们来做个总结。
this
取什么样的值,是在函数执行时确定的,不是在函数定义的时候确定的。
apply
、call
、bind
三者都是函数的方法,都可以改变函数的 this
指向。
apply
和 call
都是改变函数 this
指向,并传入参数后立即调用执行该函数。
bind
是在改变函数 this
指向后,并传入参数后返回一个新的函数,不会立即调用执行。
apply
传入的参数是数组形式的,call
传入的参数是按顺序的逐个传入并以逗号隔开, bind
传入的参数既可以是数组形式,也可以是按顺序逐个传入。
关于 this
的指向问题在前端的面试中尤为常见,大家可以按照上文中的顺序把 this
的知识点串联起来一起理解!同时,本文内容为本人理解所整理,可能会存在边界歧义等问题。如果有不理解或者有误的地方欢迎私聊我或加我微信指正~
- 公众号:星期一研究室
- 微信:MondayLaboratory
如果这篇文章对你有用,记得点个赞再走哦~
转载地址:http://piizk.baihongyu.com/