美文网首页前面面试题
前端面试之手写一个bind方法

前端面试之手写一个bind方法

作者: 88b61f4ab233 | 来源:发表于2018-10-10 16:53 被阅读10次

    bind 函数对于写react的人来说并不陌生。哦!是的,没错我的朋友,它的一个用处就是用来改变函数this指向的。如果细究一下bind的实现,发现里面还是有不少东西的,我们今天展开讨论一下。

    在说bind之前呢,我们还要先来讲讲我们的老熟人 **this。今天我们再来看看它的四种绑定规则

    This的四种绑定规则

    1.默认绑定

    独立函数调用时, this 指向全局对象,如果使用严格模式,那么全局对象无法使用默认绑定, this 绑定至 undefined 并抛错(TypeError: this is undefined)

    2.隐式绑定

    当函数作为引用属性被添加到对象中,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象

    3.显示绑定

    运用apply call 方法,在调用函数时候绑定this,也就是指定调用的函数的this值

    4.new绑定

    就是使用new操作符的时候的this绑定

    上述四条规则优先级由上到下依次递增。

    由于js多样的绑定规则,带来了 绑定隐式丢失问题, 即函数中的 this 丢失绑定对象,即它会应用第 1 条的 默认绑定 规则,从而将 this 绑定到全局对象或者 undefined 上。

    例如:绑定至上下文对象的函数被赋值给一个新的函数,然后调用这个新的函数时

    var obj = {
      a: 2,
     foo: function () {
       console.log(this.a)
      }
    }
    var a = 2
    setTimeout(obj.foo, 0) // 2
    

    还记得我们当年我们是怎么做的吗?

    ...
    var me = this;
    return function () {
      me.xxx()
    }
    ...
    

    还有就是用call 或者apply来显示的绑定:

    function foo() {
      console.log( this.a);
      }
    var obj = {
      a: 2
      };
    var bar = function() {
      foo.call(obj);
      };
    bar();  // 2
    setTimeout(bar, 100);  // 2
    

    由于这种用法太多了,所以呢ES5的时候给出了一个方法 Function.prototype.bind() ,自从有了它,前端工程师的生活似乎好过了很多,一个bind可以解决很多问题。

    先看bind定义

    MDN 给出的定义是:

    bind() 方法创建一个新的函数, 当这个新函数被调用时其this置为提供的值,其参数列表前几项置为创建时指定的参数序列。

    fun.bind(thisArg[, arg1[, arg2[, ...]]])
    

    bind() 函数会创建一个新 绑定函数绑定函数 与被调函数具有相同的函数体(在 ECMAScript 5 中)。调用 绑定函数 通常会导致执行 包装函数 绑定函数 也可以使用new运算符构造:这样做就好像已经构造了目标函数一样。提供的 this 值将被忽略,而前置参数将提供给模拟函数

    总的来说bind有如下三个功能点:

    1. 改变原函数的 this 指向,即绑定上下文,返回原函数的拷贝
    2. 绑定函数 被调用时,bind的额外参数将置于实参之前传递给被绑定的方法。
    3. 注意,一个 绑定函数 也能使用 new 操作符创建对象,这种行为就像把原函数当成构造器,thisArg 参数无效。也就是 new 操作符修改 this 指向的优先级更高。

    说了那么多,我们现在可不可以实现一个bind呢?

    其实有时候去研究这些JS API的实现还是蛮好玩的,你能学到很多知识。今天我们就手摸手写一下吧。

    从bind的定义描述中可以看到,我们要写的这个函数的输入输出基本确定了:

    • 输入:接受一个或者多个参数,第一个是要绑定的上下文,额外参数当作绑定函数的前置参数。
    • 输出:返回原函数的拷贝,即返回一个函数,这个函数呢具备原函数的功能
    // 定义这个方法为myBind
    Function.prototype.myBind = function(thisArg) {
      if (typeof this !== 'function') {
        return;
      }
      var _self = this;
      var args = Array.prototype.slice.call(arguments, 1) //从第二个参数截取
      return function() {
        return _self.apply(thisArg, args.concat(Array.prototype.slice.call(arguments))); // 注意参数的处理
      }
    }
    

    我们来测试一下:

    function foo(name) {
    this.name = name;
    }
    
    var obj = {}
    
    //上下文 功能  done
    var bar = foo.myBind(obj)
    bar('jack')
    console.log(obj.name) //'jack'
    
    // 参数 功能   done
    var tar = foo.myBind(obj, 'rose');
    tar()
    console.log(obj.name) //'rose'
    // new 功能   error
    var alice = new bar('alice')
    console.log(obj.name) //alice   obj name should be 'jack'
    console.log(alice.name) //undefined, alice name should be 'alice'
    

    可以看到使用 new 实例化被绑定的方法,上下文还指向了传入的obj,这个方法有点问题,我们需要考虑到的是在myBind的实现里面,需要检测new的操作

    我们先考虑一下new操作符在调用构造函数时做了哪些操作?

    比如说 var a = new b()

    {}
    .__proto__ = b.prototype
    

    所以我们做了如下修改:

    Function.prototype.myBind = function(thisArg) {
      if (typeof this !== 'function') {
        return;
      }
      var _self = this;
      var args = Array.prototype.slice.call(arguments, 1)
      var fnBound = function () {
        // 检测 New
        // 如果当前函数的this指向的是构造函数中的this 则判定为new 操作
        var _this = this instanceof _self ? this : thisArg;
        return _self.apply(_this, args.concat(Array.prototype.slice.call(arguments)));
      }
      // 为了完成 new操作
      // 还需要做一件事情 执行原型 链接 (思考题,为什么?
      fnBound.prototype = this.prototype;
      return fnBound;
    }
    

    测试OK了:

    function foo(name) {
    this.name = name;
    }
    var obj = {};
    var bar = foo.myBind(obj);
    bar('Jack');
    console.log(obj.name);  // Jack
    var alice = new bar('Alice');
    console.log(obj.name);  // Jack
    console.log(alice.name);    // Alice
    

    这个用例中我们来讨论一下

    bar的this指向

    • 变量 bar 是绑定之后的函数,也就是 fnBound_self原函数 foo 的引用
    • 如果直接 bar('jack') 它指向window 或undefined。
    • new bar('alice') (相当于 new foo('alice') )过程中, fnBound 的this指向了new表达式返回的对象alice
    • 如果是 new 调用绑定函数,此时绑定函数中的 this 是由 new 调用绑定函数返回的实例对象,这个对象的构造函数是 fnBound
    • 当我们忽略掉原型连接那行代码时,其原型对象并不等于原函数 self 的原型,所以 this instanceof self ? this : oThis 得到的值还是指定的传入的对象,而不是 new 返回的对象

    所以fnBound.prototype = this.prototype 是有必要的,!!但注意 这个原型赋值是有问题的:

    原因我在这里先不说了留给各位讨论啦。

    Function.prototype.myBind = function(thisArg) {
      if (typeof this !== 'function') {
        return
      }
      var _self = this
      var args = Array.prototype.slice.call(arguments, 1)
      var fnNop = function () {} // 定义一个空函数
      var fnBound = function () {
        var _this = this instanceof _self ? this : thisArg
    
        return _self.apply(_this, args.concat(Array.prototype.slice.call(arguments)))
      }
      // 维护原型关系
      if (this.prototype) {
        fnNop.prototype = this.prototype;
      }
    
      fnBound.prototype = new fnNop();
    
      return fnBound;
    }
    

    这里我们创建了一个空函数来做中间人,承接原函数的原型丢给绑定函数,好了问题就搞完了,是不是很多知识点慢慢看吧。
    如果你觉得不错,或者发现文章中的错误,或者有更好的建议,欢迎评论或进前端全栈群:866109386,来交流讨论吹水。

    相关文章

      网友评论

        本文标题:前端面试之手写一个bind方法

        本文链接:https://www.haomeiwen.com/subject/vkfmaftx.html