面试之改变this指针的方法

434 阅读6分钟

一、先说下this是什么?

定义:this是函数内部的一个特殊对象,this引用的是函数执行的环境对象。

比如在浏览器中,全局调用一个函数的话,他的执行环境对象就是window,所以此刻this = window。看个例子

window.color = "red";
var o = { color: "blue" };
function sayColor(){
    alert(this.color);
}
sayColor();     //"red"  执行环境为全局, 所以this = window
o.sayColor = sayColor;
o.sayColor();   //"blue" 执行环境是o对象,所以this = o

👆上面例子中,sayColor()在全局执行,所以this.color 就是window.color 就是red。而o.sayColor 的执行环境是o对象,所以this=o,o.color就是blue。

函数内部还有另一个特殊对象arguments,是个类数组。
可以通过ES6的Array.from()或者ES5的[].slice.call(arguments)实现转成真正的数组。在👇下面bind的实现里有这个转换的具体应用

另外它有个属性callee,指向拥有arguments对象的函数,也就是正在执行的函数。常用arguments.callee来实现和函数名的解耦, 比如阶乘递归函数 阶乘递归的具体实现


二、改变this指针的5个方法

均以👆上面例子为例

1. 函数方法apply

函数内部的方法:apply(运行函数的作用域,参数数组)

sayColor.apply(this) // red

2. 函数方法call

与apply唯一的不同就是第二个参数是逐个参数传入的

sayColor.call(o) // blue

JS原文:每个函数都有2个非继承方法,apply和call,他们的用途都是在特定的作用域中调用函数,实际上等于设置函数体内的this对的值。apply和call的作用相同,只是接受参数的方式不同,call的第一个值和apply一样,第二个值apply接受一个数组或者arguments对象,call是参数必须逐个列举出来。他们的真正的强大的地方就是能够扩充函数赖以运行的作用域

3. ES5定义的bind函数

ES5定义的的方法,这个方法会创建一个函数的实例,其this值会被绑定传给bind()函数的值。

var objectSayColor = sayColor.bind(o);
objectSayColor();    //blue

4. this赋值给闭包能访问到的变量中

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        return function(){
            return this.name;
        };
} };
alert(object.getNameFunc()()); //"The Window"         

按照this指向运行环境的对象来说,上面的this应该指向getNameFunc,但是由于

匿名函数执行环境具有全局性,所以this= window

所以 this.name 就是 The Window。所以要得到My Object,应该

 var that = this;
 return function(){
    return that.name;
};

顺便看下匿名函数和闭包都是什么?

4.1 匿名函数:函数表达式的创建方式创建出来的函数

创建一个函数赋值给变量的创建方式就是一个匿名函数。

const s = function(){}这就是一个匿名函数,因为function关键字后面没有标识符

4.2 闭包:有权访问另一函数作用域内变量的函数

  • 创建闭包的最常见方式:一个函数内创建另一个函数
  • 副作用:闭包只能取得包含函数中任何变量的最后一个值
  • 解决方案:通过创建另一个匿名函数强制让闭包的行为符合预期

eg: 点击页面的li列表弹出每个li的索引

var arr = document.getElementByTagName('ul')[0].querySelectorAll('li')
<!--错误案例❎-->
for(var i =0;i<arr.length;i++){
    arr[i].onclick = function(){  //闭包只能取得包含函数最后一个值
       console.log(i)   //全都是3
    }
}
<!--解决方案一:✅-->
for(var i =0;i<arr.length;i++){
    arr[i].onclick = function(num){  //创建另一个匿名函数
       return function(){
            console.log(num)   //0,1,2
       }
    }(i)
}
<!--解决方案二: ES6 的let ✅-->
for(let i =0;i<arr.length;i++){  // let 只在所在的命令块中有效
    arr[i].onclick = function(){  
       console.log(i)   //0,1,2
    }
}

5. ES6 箭头函数

  • 函数体内的this对象, 就是定义所在的对象,不是使用所在的对象
  • 不可以当做构造函数,也就是不能new
  • 不可以使用arguments对象,该对象在函数体内不存在,如果要用可以用reset代替
  • 不可以使用yield命令,因此箭头函数不能作为generator函数
setTimeout(()=>{
    console.log("箭头函数this是固定的")
})

三、自己实现一个bind函数[简单版本]

了解了函数的一些属性和方法以及改变this指针的一些方法,下面让我们来实践一下吧,自己实现一个简单的bind绑定函数吧!

使用方式sayColor.bind(o)();

思路如下

  • 肯定在Function.prototype 上
  • 可以改变context环境对象, 也就是说需要用到apply
  • 实现柯里化,也就是传参后调用的时候也传入参数:需要合并所有的参数
var foo = {
    color: 'red',
    sayColor: function () {
        console.log(this.color)
        console.log(Array.from(arguments))
    }
}
Function.prototype.mybind = function(context){
    var that = this; 
    var args = [].slice.call(arguments,1) //第一个参数是环境变量
    return function(){
        var all_args = args.concat (Array.from(arguments))
        that.apply(context,all_args)
    }
}
foo.sayColor.mybind(foo,'参数1','参数2')('柯里式')

bind函数更优版,暂未开启

四、函数的其他属性

以上的arguments、this、apply、call都是函数的方法或者对象,除非之外函数还有以下三个属性

1.函数caller属性

  • ES5 规范的函数对象的属性:caller。ES3中没有定义
  • opera早起不支持,其他浏览器都支持
  • 保存着 【调用当前函数的】的函数的引用
  • 如果在全局作用域中调用当前函数,值为null
  • 严格模式下:arguments.callee 会报错
  • 严格模式下:不能为函数的caller属性赋值,否则会导致错误
  • ES5还定义了arguments.caller。严格模式下报错,非严格下undefined 。定义这个属性只是为了区分函数的caller

以上变化都是为了加强语言的安全性,这样第三方代码就不能在相同的环境中窥视其他代码了。

function outer(){
    inner();
}
function inner(){
    alert(arguments.callee.caller);  // 其实就是inner.caller
} 
outer();

2. length:表示函数希望接受的命名参数个数

3. prototype:原型

  • 是保存实例化方法的真正所在。
  • 比如toString(),valueOf()都是在这上面。
  • prototype属性不可枚举,因此for-in不能发现

在创建自定义引用类型,以及实现继承上,非常重要,这里暂时先不介绍了

五、总结

看上以上内容,可以回答下下面的面试题吗?

  1. 你知道闭包吗?

闭包就是有权访问另一函数作用域内变量的函数,最常见的创建方法就是函数内创建另一个函数,副作用是闭包只能取得包含函数中任何变量的最后一个值,比如for循环,解决方案是内部创建一个匿名函数并把参数穿进去或者let。

  1. 改变this指针的方法
  • apply
  • call
  • 闭包内赋值外部作用域this
  • bind函数
  • ES6的箭头函数
  1. 自己实现一个bind函数: 看👆上面
  2. 写一下实现3!的代码