• home > webfront > ECMAS > javascript >

    JS中的bind的用法与注意事项,如何原生实现bind方法

    Author:zhoulujun@live.cn Date:

    众所周知 在jQuery和prototype js之类的框架里都有个bind jQuery里的用途是给元素绑定事件,在EcmaScript5中也扩展了叫bind的方法(IE6,7,8不支持),使用方法如下

    call apply

    call apply JavaScript 实现

    Function.call = function (context, ...rest) { // apply ,把参数...rest 改为 arr 即可
      context && Object(context) || (context = window); 
      context.fn = this;
      let result = eval('content.fn(' + rest + ')');
      delete context.fn;
      return result;
    };

    call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。

    Function.prototype.bind()

    bind()方法主要就是将函数绑定到某个对象,bind()会创建一个函数,函数体内的this对象的值会被绑定到传入bind()中的第一个参数的值

    改变执行上下文,并返回bind后的方法


    bind()函数的两个特性:

    在 bind 的时候,只传一个 name,在执行返回的函数的时候,再传另一个参数

    var foo = {value: 1};
    function bar(name, age) {
        console.log(this.value);
        console.log(name);
        console.log(age);
    }
    
    var bindFoo = bar.bind(foo, 'daisy');
    bindFoo('18');
    // 1 daisy 18

    这个特性,可以做很多事情

    function add(a, b, c) {
        var i = a+b+c;
        console.log(i);
        return i;
    }
    var func = add.bind(undefined, 100);//给add()传了第一个参数a
    func(1, 2);//103,继续传入b和c
    var func2 = func.bind(undefined, 200);//给func2传入第一个参数,也就是b,此前func已有参数a=100
    func2(10);//310,继续传入c,100+200+10

    前面几项固定的配置可以选择用bind函数先绑定好

    bind的简易实现

    // 第一版
    Function.prototype.bind2 = function (context, ...rest) {
      var self = this;
      return function () {
        self.apply(context, rest.concat([].slice.call(arguments)));
      };
    };

    因为 bind 还有一个特点,就是

    一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

    也就是说当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效。

    var value = 2;
    var foo = {
      value: 1
    };
    function bar(name, age) {
      this.habit = 'shopping';
      console.log(this.value);
      console.log(name);
      console.log(age);
    }
    bar.prototype.friend = 'kevin';
    var bindFoo = bar.bind(foo, 'daisy');
    var obj = new bindFoo('18');
    // undefined
    // daisy
    // 18

    注意:尽管在全局和 foo 中都声明了 value 值,最后依然返回了 undefind,说明绑定的 this 失效了,如果大家了解 new 的模拟实现,就会知道这个时候的 this 已经指向了 obj。

    bind和new

    bind返回的函数是没有原型的。但bind后方法new出来的对象拥有和原函数一样的方法

    (这其实是个很有用的设计,让一切都发生在原来函数上,这样出现bug也更容易查找,不用关心绑定后的函数。)

    function foo() {
        this.b = 100;
        console.log(this.a);
        return this.a;
    }
    var func =  foo.bind({a:1});
    func();//1
    new func();//undefined   {b:100},可以看到此时上面的bind并不起作用

    函数中的return除非返回的是个对象,否则通过new返回的是个this,指向一个空对象,空对象原型指向foo.prototype,空对象的b属性是100。也就是说通过new的方式创建一个对象,bind()函数在this层面上并不起作用,但是需要注意在参数层面上仍起作用,如下:

    function foo(c) {
        this.b = 100;
        console.log(this.a);
        console.log(c);
        return this.a;
    }
    
    var func =  foo.bind({a:1},20);
    new func();//undefined 20,通过new创建对象func,bind绑定的c依旧起作用

    了解完以上两个特性,再来看看bind()的实现:

    if (!Function.prototype.bind) {
      Function.prototype.bind = function (context) {
      if (typeof this !== "function") {
        throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
      }
      var self = this;
      var args = Array.prototype.slice.call(arguments, 1);
      var fNOP = function () {};
    
      var fbound = function () {
        self.apply(this instanceof self ? this : context, args.concat(Array.prototype.slice.call(arguments)));
      }
    
      fNOP.prototype = this.prototype;
      fbound.prototype = new fNOP();
    
      return fbound;
    }
    }

    多次绑定的结果:无论使用bind绑定多少次,最终原函数的this值是由第一次绑定传的参数决定的。

    多次绑定参数的顺序:内置的参数 + 调用时传入的参数 = 最终函数(arguments类似array concat)



    张鑫旭的文章:


    众所周知 在jQuery和prototype.js之类的框架里都有个bind

    jQuery里的用途是给元素绑定事件

    $("#scroll").bind("click", function() {});
    
    但是:
    说的不是这个:
    在EcmaScript5中也扩展了叫bind的方法(IE6,7,8不支持)


    张鑫旭 bind方法

    但是看完后,还是没有弄明白…………张鑫旭!你忽悠我玩啊……

    好吧…不能抱怨,神的世界,我不了解……

    举个栗子……


    function T(c) {

        this.id = "Object";

        this.dom = document.getElementById("scroll");

    }

    T.prototype = {

        init: function() {

           //①

            this.dom.onmouseover = function() {

                console.log("Over-->"+this.id);

            }

           //②

            this.dom.onmouseout = function() {

                console.log("Out -->"+this.id);

            } .bind(this)

        }

    };

    (new T()).init();

    over----》scroll    out--》Object

    知道,bindcall,apply很相似,,例如,可接受的参数都分为两部分,且第一个参数都是作为执行时函数上下文中的this的对象。其它,母鸡了……

    然后,去MDN看……


    明白 bind 的用法就必须要知道 apply 的用法,MDN 指出,apply 是直接修改了函数内部的指向到第一个参数,并将第二个参数数组传参进函数并运行这个函数。也就是说

    var obj = {test: function() { console.log(this, arguments) }},
        func = obj.test;
    
    obj.test("Hello", ",", "world", "!");
    func.apply(obj, ["Hello", ",", "world", "!"]);

    这两种运行方式是一样的。那么回到 Polyfill 中发现参数的写法是 args.concat(slice.call(arguments))args 是将 bind 时候定义的除第一个参数外的其它参数,而此时的 arguments 是指函数调用时候的参数,通过数组的操作将这两个参数合并成一个数组传入函数内部。看个例子你可能更容易明白:

    /** 代码接上 **/var newFunc = func.bind(obj, "Hello", ",");
    newFunc("world", "!");

    那么再来回答问题一,这个是典型的属性继承的方法,本来使用

    bound.prototype = self.prototype

    就可以将原属性集成过来了,但是这样两个对象属性都指向同一个地方,修改 bound.prototype 将会造成self.prototype 也发生改变,这样并不是我们的本意。所以通过一个空函数 nop 做中转,能有效的防止这种情况的发生。

    ………………

    20160517120605108.png

    尼玛,还是看不懂……


    js里函数调用有 4 种模式: 方法调用 、 正常函数调用 、 构造器函数调用 、apply/call 调用 。

    同时,无论哪种函数调用除了你声明时定义的形参外,还会自动添加 2 个形参,分别是 this 和 arguments 。

    arguments不涉及到上述 3 个函数,所以这里只谈 this 。 this 的值,在上面 4 中调用模式下,分别会绑定不同的值。分别来说一说:

    方法调用 

    这个很好理解,函数是一个对象的属性,比如

    var a = {    
        v : 0,    
        f : function(xx) {                
            this.v = xx;    
        }
    }
    a.f(5);

    这个时候,上面函数里的this就绑定的是这个对象a。所以this.v可以取到对象a的属性v。

    正常函数调用: 
    依然看代码

    function f(xx) {        
        this.x = xx;
    }
    f(5);

    这个时候,函数f里的this绑定的是全局对象,如果是在浏览器运行的解释器中,一般来说是 window对象。所以这里this.x访问的其实是window.x,当然,如果window没有x属性,那么你这么一写,按照js的坑爹语法,就是给window对象添加了一个x属性,同时赋值。

    构造器函数调用 : 
    构造函数一直是我认为是 js 里最坑爹的部分,因为它和 js 最初设计的基于原型的面向对象实现方式格格不入,就好像是特意为了迎合大家已经被其他基于类的面相对象实现给惯坏了的习惯。 
    如果你在一个函数前面带上 new 关键字来调用,那么 js 会创建一个 prototype 属性是此函数的一个新对象,同时在调用这个函数的时候,把 this 绑定到这个新对象上。当然 new 关键字也会改变 return 语句的行为,不过这里就不谈了。看代码

    function a(xx) {        
        this.m = xx;
    }
    var b = new a(5);

    上面这个函数和正常调用的函数写法上没什么区别,只不过在调用的时候函数名前面加了关键字new罢了,这么一来,this绑定的就不再是前面讲到的全局对象了,而是这里说的创建的新对象,所以说这种方式其实很危险,因为光看函数,你不会知道这个函数到底是准备拿来当构造函数用的,还是一般函数用的。所以我们可以看到,在jslint里,它会要求你写的所有构造函数,也就是一旦它发现你用了new关键字,那么后面那个函数的首字母必须大写,这样通过函数首字母大写的方式来区分,我个人只有一个看法:坑爹:)

    apply/call 调用: 
    我们知道,在 js 里,函数其实也是一个对象,那么函数自然也可以拥有它自己的方法,有点绕,在 js 里,每个函数都有一个公共的 prototype ―― Function ,而这个原型自带有好几个属性和方法,其中就有这里困惑的 bind 、 call 、 apply 方法。先说 apply 方法,它让我们构造一个参数数组传递给函数,同时可以自己来设置 this 的值,这就是它最强大的地方,上面的 3 种函数调用方式,你可以看到, this 都是自动绑定的,没办法由你来设,当你想设的时候,就可以用 apply() 了。 apply  函数接收 2 个参数,第一个是传递给这个函数用来绑定  this 的值,第二个是一个参数数组。看代码

    function a(xx, yy) {	  
      alert(xx, yy);	
      alert(this);	
      alert(arguments);
    }
    a.apply(null, [5, 55]);
    a.call(null, 5, 55);

    是不是很神奇,函数a居然可以给o加属性值。当然,如果你apply的第一个参数传递null,那么在函数a里面this指针依然会绑定全局对象。

    call() 方法和 apply() 方法很类似,它们的存在都是为了改变 this 的绑定,那 call()和 apply() 有什么区别呢?就我个人看来,没啥鸟区别。。。开玩笑!刚刚说了,上面 apply() 接收两个参数,第一个是绑定  this 的值,第二个是一个参数数组,注意它是一个数组,你想传递给这个函数的所有参数都放在数组里,然后 apply() 函数会在调用函数时自动帮你把数组展开。而 call() 呢,它的第一个参数也是绑定给 this 的值,但是后面接受的是不定参数,而不再是一个数组,也就是说你可以像平时给函数传参那样把这些参数一个一个传递。所以如果一定要说有什么区别的话,看起来是这样的

    function a(xx, yy) {	
      alert(xx, yy);	
      alert(this);	
      alert(arguments);
    }
    a.apply(null, [5, 55]);
    a.call(null, 5, 55);

    仅此而已。

    最后再来说bind()函数,上面讲的无论是call()也好,apply()也好,都是立马就调用了对应的函数,而bind()不会,bind()会生成一个新的函数,bind()函数的参数跟call()一致,第一个参数也是绑定this的值,后面接受传递给函数的不定参数。bind()生成的新函数返回后,你想什么时候调就什么时候调,看下代码就明白了

    var m = {	
      "x" : 1
    };
    function foo(y) {	
      alert(this.x + y);
    }
    foo.apply(m, [5]);
    foo.call(m, 5);
    var foo1 = foo.bind(m, 5);
    foo1();

    末了来个吐槽,你在js里想定义一个函数,于是你会这么写: 

    function jam() {};

    其实这是js里的一种语法糖,它等价于: 

    var jam = function() {};

    然后你想执行这个函数,脑洞大开的你会这么写: 

    function jam() {}();

    但是这么写就报错了,其实这种写法也不算错,因为它确实是js支持的函数表达式,但是同时js又规定以function开头的语句被认为是函数语句,而函数语句后面是肯定不会带 () 的,所以才报错,于是聪明的人想出来,加上一对括号就可以了。于是就变成了这样: 

    (function jam() {}());

    不要滥用bind方法

    我测试了一下浏览器原生的Function.prototype.bind,发现使用了bind之后,函数的内存占用增加了近2倍!CoffeeScript实现的绑定稍微轻量一点,内存占用也增加了1倍多。


    再顺便测试了下ES6新增的Arrow function(也是=>),因为这个特殊函数是自带绑定技能的,结果惊奇地发现,它的内存占用和普通的Function没啥区别。所以以后需要或者不需要bind的场景如果一定要滥用bind图个安心的话,可以通通上高逼格的箭头函数。:)


    参考文章:

    JavaScript中的bind方法及其常见应用 https://www.cnblogs.com/goloving/p/8542781.html

    JavaScript深入之bind的模拟实现  https://juejin.im/post/59093b1fa0bb9f006517b906

    原生JS实现bind()函数https://www.cnblogs.com/goloving/p/9380076.html

    Javascript中bind方法原生实现 https://zhuanlan.zhihu.com/p/49924137

    JavaScript深入之call和apply的模拟实现 https://github.com/mqyqingfeng/Blog/issues/11


    转载本站文章《JS中的bind的用法与注意事项,如何原生实现bind方法》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/js/2016_0517_7828.html