面试题热点话题,this
指向、如何改变this
指向、call()
apply()
bind()
区别,还有手写call()
apply()
以及bind()
。
一、 this指向问题,这个问题主要是分一些情况来去理解的,整体而言可以理解成this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象,不过具体的特殊情况还需要特殊对待。更多关于this指向问题可以参考文章 《彻底理解js中this的指向》。
二、 call() apply()
和bind()
的区别
相同点:
都可以改变函数的this
指向 第一个参数都是this要指向的对象。
不同点:
call()
和apply()
接收参数不同:
call()
接收多个参数,第一个参数为this
要指向的对象,第二个参数为调用调用call
方法的函数的第一个参数,第三个参数为该函数的第二个参数,依次类推。
apply()
接收两个参数,第一个参数为this
要指向的对象,第二个参数为一个数组,数组的第一项为调用 apply
方法的第一个参数,数组第二项为该函数的第二个参数,依次类推。
bind()
第一个参数是this
要指向的对象,第二个参数为调用调用bind
方法的函数的第一个参数,第三个参数为该函数的第二个参数,依次类推。
call()
apply()
和bind()
执行时机不同:
call()
apply()
在被函数调用后立即执行,而bind()
方法则是返回一个新的函数(未执行)。
三、 call()
、apply()
、bind()
语法
call()
语法:
var obj = {
name:"lucy"
}
function FunCall(a,b,c){
console.log(this.name); //lucy
console.log("参数",a,b,c) // a b c
}
FunCall.call(obj,'a','b','c')
apply()
语法:
var obj = {
name:"lucy"
}
function FunApply(a,b,c){
console.log(this.name); //lucy
console.log("参数",a,b,c) // a b c
}
Funapply.Apply(obj,["a","b","c"])
bind()
语法:
var obj = {
name:"lucy"
}
function FunBind(a,b,c){
console.log(this.name); //lucy
console.log(a,b,c); // a b c
}
var funbind = FunBind.bind(obj,'a','b','c')
funbind();
注意:bind()
方法还可以这样传参
var obj = {
name:"lucy"
}
function FunBind(a,b,c){
console.log(this.name); //lucy
console.log(a,b,c); // a b c
}
var funbind = FunBind.bind(obj)
funbind('a','b','c');
bind()
除了this
还接收其他参数,bind()
返回的函数也接收参数。
以上是对call()
,apply()
,bind()
的异同以及语法使用,需要了解更多call()
,apply()
,bind()
的信息,可以参考 MDN 对这几个方法更详细的的说明。
四、 手写call()
、apply()
、bind()
方法。
- 手写
call()
:
在写之前思考一下,call()
方法实际上做的事情就是改变了this
的指向,那如何改变一个函数的this
指向呢?就是改变函数的调用者
。
按照这个思路来看下面代码。
Function.prototype.mycall = function (thisArg,...args){
//先明白我们的参数 都带代表什么,thisArg代表this当前需要指向的对象,args表示该函数接收的参数。
//我们参照call 方法理解 foo.call(obj,"a","b") , 此时thisArg就代表obj,...args 就代表参数啊 a,b
var fn = Symbol("fn");
//创建一个变量,使用Symbol是因为保证它的唯一性,不和thisArg其他相同fn属性冲突
var thisArg = thisArg || window;
// 当thisArg不存在时也就是调用call()没传第一个参数时,默认为window。
thisArg[fn] = this;
//这里的这个this代表的是调用call方法的函数,比如foo.call() this代表foo函数,通过赋值操作,此时对象thisArg下面的fn属性就是一个方法
var result = thisArg[fn](...args);
//这一步是核心,调用对象thisArg下的fn方法,在这一步函数foo的this已改变为thisArg
delete thisArg[fn];
//当函数调用完成之后销毁掉
return result;
}
var obj = {
name:"lucy"
}
function foo(a,b){
console.log(this.name); //lucy
console.log(a,b); //a b
}
foo.mycall(obj,'a','b');
如果上述逻辑不是很清楚,就先去看一下《彻底理解js中this的指向》。再看看this指向再回来思考这段代码。
- 手写
apply()
:
apply()
跟call()
其实很相似,按照上面代码的注释理解一下下面的代码
Function.prototype.myapply = function(thisArgs,args){
var fn = Symbol("fn");
var thisArgs = thisArgs || window;
thisArgs[fn] = this;
var result = thisArgs[fn](...args);
delete thisArgs[fn];
return result;
}
var obj = {
name:"zhangsan"
}
function foo (a,b,c){
console.log(this.name); //zhangsan
console.log(a,b,c);// a b c
}
foo.myapply(obj,['a','b','c']);
- 手写
bind()
bind()
需要思考到的地方比较多,先一点点来。
第一步借助实现bind()
基本功能改变this指向。
Function.prototype.mybind = function(thisArgs){
var args = Array.prototype.slice.call(arguments,1);
var _this = this;
return function(){
_this.apply(thisArgs,[...args]) //this指向被改变到thisArgs
}
}
var obj = {
name:"lili"
}
function foo(a,b,c){
console.log(this.name); // lili
console.log(a,b,c); // a b c
}
var ffff = foo.mybind(obj,'a',"b","c")
ffff();
由代码可知bind()
的改变this功能实现了
这里会有个问题
看下面代码
Function.prototype.mybind = function(thisArgs){
var args = Array.prototype.slice.call(arguments,1);
var _this = this;
return function(){
_this.apply(thisArgs,[...args]) //this指向被改变到thisArgs
}
}
var obj = {
name:"lili"
}
function foo(a,b,c){
console.log(this.name); // lili
console.log(a,b,c); // undefined undefined undefined
}
var ffff = foo.mybind(obj)
ffff("a","b","c");//当把参数放到这里时会发生什么?
//看上面输出,参数没传递进去,但是人家原生bind是可以在绑定this之后再传参的,不信可以试试。
接着来完善
Function.prototype.mybind = function(thisArgs){
var args = Array.prototype.slice.call(arguments,1);
var _this = this;
return function(){
_this.apply(thisArgs,[...args,...arguments]) //这里与上面代码不同的地方,这里获取到当前函数的参数,并一并传递给函数执行。
}
}
var obj = {
name:"lili"
}
function foo(a,b,c){
console.log(this.name); // lili
console.log(a,b,c); // a b c
}
var ffff = foo.mybind(obj)
ffff("a","b","c");//此时在这里传参
再接着完善:
我们知道对一个函数使用new
操作符,会生成一个对象,该对象称为当前函数的实例。该函数可以称为构造函数,构造函数里的this
指向当前的实例化对象。
function Person(name){
this.name = "sss"
}
var person = new Person()
console.log(person) // { name:"sss" }
如果 ffff
函数被当做构造函数使用new
操作符产生一个实例化对象,按照道理来说this应该指向实例化对象,但是我们使用bind()
方法函数foo
的this指向了对象obj
,因此我们需要处理一下当ffff
函数被使用new
操作符生成实例化对象
时,改变this
指向,让它指向当前实例化对象
而非obj
。
Function.prototype.mybind = function(thisArgs){
if(typeof this !== "function"){
return new Error('not a function')
}
var args = Array.prototype.slice.call(arguments,1);
var _this = this;
var result = function (){ //稍微做了改变将函数赋值给result变量
_this.apply(thisArgs,[...args,...arguments])
}
return result;
}
var obj = {
name:"lucy"
}
function foo(name){
this.name = name;
}
var ffff= foo.mybind(obj);
var instance = new ffff("jack"); //实例化被bind()方法绑定this之后返回的函数
console.log(obj) //{name:"jack"} //理想输出应该是 {name:"lucy"}
console.log(instance); // {} //理想输出应该是 { name:"jack" }
此时可以使用原生bind()
方法观察比较
var obj = {
name:"lucy"
}
function foo(name){
this.name = name;
}
var ffff= foo.bind(obj); //用原生bind()方法
var instance = new ffff("jack"); //实例化被bind()方法绑定this之后返回的函数
console.log(obj) //{ name:"lucy" }
console.log(instance); // { name:"jack" }
这下很直观的能观察到我们的mybind方法有问题,问题就在当ffff
函数被使用new
操作符生成实例化对象
时,没有改变this
指向到实例化对象,而还是指向mybind()
的第一个参数obj
。
如何改变this呢?看一下代码
Function.prototype.mybind = function(thisArgs){
if(typeof this !== "function"){
return new Error('not a function')
}
var args = Array.prototype.slice.call(arguments,1);
var _this = this;
var result = function (){
var thisArgs = this instanceof result ? this : thisArgs;
/*
在这里对即将传入apply的第一个参数进行判断,
判断当前this是否是result函数的实例,
如果是那就将当前的this传递进去,
如果不是那就还是使用传递进来的第一个参数。
*/
_this.apply(thisArgs,[...args,...arguments])
}
return result;
}
var obj = {
name:"lucy"
}
function foo(name){
this.name = name;
}
foo.prototype.age = 18; // 第26行
var ffff = foo.mybind(obj);
var instance = new ffff("jack");
console.log(obj) //{name:"lucy"}
console.log(instance); // { name:"jack" }
console.log(instance.age);// undefined(先不用看这个) // 第33行
我们在第9行加了一个判断,判断这里的this
是不是指向当前函数的实例,如果是,那么我们就需要改变传入apply
的第一个参数了。
最终实现:
看上面代码26行我们给foo函数原型对象上写入了一个属性age
,理论上我们通过instance.age
就能访问到,但是通过第33行代码我们发现,instance
的原型链上没有age属性。
再看一下代码:
Function.prototype.mybind = function(thisArgs){
if(typeof this !== "function"){
return new Error('not a function')
}
var args = Array.prototype.slice.call(arguments,1);
var _this = this;
var result = function (){
console.log(this);//result{}
var alignThis = _this instanceof result ? _this : thisArgs;
_this.apply(alignThis,[...args,...arguments])
}
//将调用mybind方法的函数的原型对象利用原型继承的方式将该函数的原型继承到result的原型对象上。
var FUN = function(){} //第15行
FUN.prototype = _this.prototype;
result.prototype = new FUN() //第17行
return result;
}
var obj = {
name:"lucy"
}
function foo(name){
this.name = name;
}
foo.prototype.age = 18;
var ffff = foo.mybind(obj);
var instance = new ffff("jack");
console.log(obj)//{name:"lucy"}
console.log(instance);//{name:"jack"}
console.log(instance.age); // 18
通过15-17行,完成原型对象的继承,这样就完成了手写bind()
。
到此为止,call()
apply()
bind()
方法的手写实现也完成了。
写的有些啰嗦,但是边写边怕自己写不清楚,又怕写错了,只能慢慢一步步来,希望看到此文的伙伴有疑惑或者发现错误请不吝赐教。谢谢。
写在最后:文中内容大多为自己平时从各种途径学习总结,文中参考文章大多收录在我的个人博客里,欢迎阅览http://www.tianleilei.cn