Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Javascript之bind #1

Open
Aaaaaaaty opened this issue Jun 1, 2017 · 18 comments
Open

Javascript之bind #1

Aaaaaaaty opened this issue Jun 1, 2017 · 18 comments

Comments

@Aaaaaaaty
Copy link
Owner

Aaaaaaaty commented Jun 1, 2017

写在最前

最近开始重新学习一波js,框架用久了有些时候觉得这样子应该可以实现发现就真的实现了,但是为什么这么写好像又说不太清楚,之前读了LucasHC以及冴羽的两篇关于bind的文章感觉自己好像基础知识都还给体育老师了哈哈哈,所以危机感爆棚,赶紧重头复习一遍。本次主要围绕bind是什么;做了什么;自己怎么实现一个bind,这三个部分。其中会包含一些细节代码的探究,往下看就知道。

所以bind是什么

bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。

var result = fun.bind(thisArg[, arg1[, arg2[, ...]]]) 
result(newArg1, newArg2...)

没看懂没事接着往下看。

bind到底做了什么

从上面的介绍中可以看出三点。首先调用bind方法会返回一个新的函数(这个新的函数的函数体应该和fun是一样的)。同时bind中传递两个参数,第一个是this指向,即传入了什么this就等于什么。如下代码所示:

this.value = 2
var foo = {
    value: 1
}
var bar = function() {
  console.log(this.value)
}
var result = bar.bind(foo)
bar() // 2
result() // 1,即this === foo

第二个参数为一个序列,你可以传递任意数量的参数到其中。并且会预置到新函数参数之前。

this.value = 2
var foo = {
    value: 1
};
var bar = function(name, age, school) {
  console.log(name) // 'An'
  console.log(age) // 22
  console.log(school) // '家里蹲大学'
}
var result = bar.bind(foo, 'An') //预置了部分参数'An'
result(22, '家里蹲大学') //这个参数会和预置的参数合并到一起放入bar中

我们可以看出在最后调用result(22, '家里蹲大学')的时候,其内部已经包含了在调用bind的时候传入的 'An'

一句话总结:调用bind,就会返回一个新的函数。这个函数里面的this就指向bind的第一个参数,同时this后面的参数会提前传给这个新的函数。调用该新的函数时,再传递的参数会放到预置的参数后一起传递进新函数。

自己实现一个bind

实现一个bind需要实现以下两个功能

  • 返回一个函数,绑定this,传递预置参数
  • bind返回的函数可以作为构造函数使用。故作为构造函数时应使得this失效,但是传入的参数依然有效

1、返回一个函数,绑定this,传递预置参数

this.value = 2
var foo = {
    value: 1
};
var bar = function(name, age, school) {
    console.log(name) // 'An'
    console.log(age) // 22
    console.log(school) // '家里蹲大学'
    console.log(this.value) // 1
}
Function.prototype.bind = function(newThis) {
    var aArgs   = Array.prototype.slice.call(arguments, 1) //拿到除了newThis之外的预置参数序列
    var that = this
    return function() {
        return that.apply(newThis, aArgs.concat(Array.prototype.slice.call(arguments)))
        //绑定this同时将调用时传递的序列和预置序列进行合并
    }
}
var result = bar.bind(foo, 'An')
result(22, '家里蹲大学')

这里面有一个细节就是Array.prototype.slice.call(arguments, 1) 这句话,我们知道arguments这个变量可以拿到函数调用时传递的参数,但不是一个数组,但是其具有一个length属性。为什么如此调用就可以将其变为纯数组了呢。那么我们就需要回到V8的源码来进行分析。#这个版本的源码为早期版本,内容相对少一些。


function ArraySlice(start, end) {
  var len = ToUint32(this.length); 
  //需要传递this指向对象,那么call(arguments),
  //便可将this绑定到arguments,拿到其length属性。
  var start_i = TO_INTEGER(start);
  var end_i = len;
  
  if (end !== void 0) end_i = TO_INTEGER(end);
  
  if (start_i < 0) {
    start_i += len;
    if (start_i < 0) start_i = 0;
  } else {
    if (start_i > len) start_i = len;
  }
  
  if (end_i < 0) {
    end_i += len;
    if (end_i < 0) end_i = 0;
  } else {
    if (end_i > len) end_i = len;
  }
  
  var result = [];
  
  if (end_i < start_i)
    return result;
  
  if (IS_ARRAY(this))
    SmartSlice(this, start_i, end_i - start_i, len, result);
  else 
    SimpleSlice(this, start_i, end_i - start_i, len, result);
  
  result.length = end_i - start_i;
  
  return result;
};

从源码中可以看到通过call将arguments下的length属性赋给slice后,便可通过 start_i & end_i来获得最后的数组,所以不需要传递进slice时就是一个纯数组最后也可以得到一个数组变量。

2、bind返回的函数可以作为构造函数使用

被用作构造函数时,this应指向new出来的实例,同时有prototype属性,其指向实例的原型。

this.value = 2
var foo = {
  value: 1
};
var bar = function(name, age, school) {
  ...
  console.log('this.value', this.value)
}
Function.prototype.bind = function(newThis) {
  var aArgs   = Array.prototype.slice.call(arguments, 1)
  var that = this  //that始终指向bar
  var NoFunc = function() {}
  var resultFunc = function() {
    return that.apply(this instanceof that ? this : newThis, aArgs.concat(Array.prototype.slice.call(arguments)))
  } 
  NoFunc.prototype = that.prototype //that指向bar
  resultFunc.prototype = new NoFunc()
  return resultFunc
  
}
var result = bar.bind(foo, 'An')
result.prototype.name = 'Lsc' // 有prototype属性
var person = new result(22, '家里蹲大学')
console.log('person', person.name) //'Lsc'

上面这段模拟代码做了两件重要的事。

1.给返回的函数模拟一个prototype属性。,因为通过构造函数new出来的实例可以查询到原型上定义的属性和方法

var NoFunc = function() {}
...
NoFunc.prototype = that.prototype //that指向bar
resultFunc.prototype = new NoFunc()
return resultFunc

通过上面代码可以看出,that始终指向bar。同时返回的函数已经继承了that.prototype即bar.prototype。为什么不直接让返回的函数的prototype属性resultFunc.prototype 等于为bar(that).prototype呢,这是因为任何new出来的实例都可以访问原型链。如果直接赋值那么new出来的对象可以直接修改bar函数的原型链,这也就是是原型链污染。所以我们采用继承的方式(将构造函数的原型链赋值为父级构造函数的实例),让new出来的对象的原型链与bar脱离关系。

2.判断当前被调用时,this是用于普通的bind还是用于构造函数从而更改this指向。

如何判断当前this指向了哪里呢,通过第一点我们已经知道,通过bind方法返回的新函数已经有了原型链,剩下需要我们做的就是改变this的指向就可以模拟完成了。通过什么来判断当前被调用是以何种姿势呢。答案是instanceof

instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

// 定义构造函数
function C(){} 
function D(){} 
var o = new C();
// true,因为 Object.getPrototypeOf(o) === C.prototype
o instanceof C; 
// false,因为 D.prototype不在o的原型链上
o instanceof D; 

从上面可以看出,instanceof可以判断出一个对象是否是由这个函数new出来的,如果是new出来的,那么这个对象的原型链应为该函数的prototype.
所以我们来看这段关键的返回的函数结构:

var resultFunc = function() {
    return that.apply(this instanceof that ? 
        this : 
        newThis, 
        aArgs.concat(Array.prototype.slice.call(arguments)))
  } 

在这其中我们要先认清this instanceof that 中的this是bind函数被调用后,返回的新函数中的this。所以这个this可能执行在普通的作用域环境,同时也可能被new一下从而改变自己的指向。再看that,that始终指向了bar,同时其原型链that.prototype是一直存在的。所以如果现在这个新函数要做new操作,那么this指向了新函数,那么 this instanceof that === true, 所以在apply中传入this为指向,即指向新函数。如果是普通调用,那么this不是被new出来的,即新函数不是作为构造函数,this instanceof that === false就很显而易见了。这个时候是正常的bind调用。将调用的第一个参数作为this的指向即可。

完整代码(MDN下的实现)

if (!Function.prototype.bind) {
  Function.prototype.bind = function(oThis) {
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }

    var aArgs   = Array.prototype.slice.call(arguments, 1),
        fToBind = this,
        fNOP    = function() {},
        fBound  = function() {
          return fToBind.apply(this instanceof fNOP
                 ? this
                 : oThis,
                 aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    if (this.prototype) {
      // Function.prototype doesn't have a prototype property
      fNOP.prototype = this.prototype; 
    }
    fBound.prototype = new fNOP();
    return fBound;
  };
}

可以看到,其首先做了当前是否支持bind的判定,不支持再实行兼容。同时判断调用这个方法的对象是否是个函数,如果不是则报错。

同时这个模拟的方法也有一些缺陷,可关注MDN上的Polyfill部分

小结

模拟bind实现最大的一个缺陷是,模拟出来的函数中会一直存在prototype属性,但是原生的bind作为构造函数是没有prototype的,这点打印一下即可知。不过这样子new出来的实例没有原型链,那么它的意义是什么呢。如果哪天作者知道了意义会更新在这里的=。= 如果说错的地方欢迎指正,一起交流哈哈。

@Aaaaaaaty Aaaaaaaty changed the title 查缺补漏记——bind Javascript之bind——折腾记 Jun 1, 2017
@xumeiyan
Copy link

xumeiyan commented Jun 2, 2017

你好,我有个疑问。您的文章是this instanceof that,而规范是this instanceof FNOP,这两种判断有区别吗?因为that指向原始函数,FNOP指向中间函数

@Aaaaaaaty
Copy link
Owner Author

@xumeiyan 我认为你说的就是可能就是下面的意思

var A = function() {}
var B = function() {}
var C = function() {}
B.prototype = A.prototype
C.prototype = new B()
var D = new C()
console.log(D instanceof C) //true
console.log(D instanceof B) //true

看下这个定义

instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

所以上面两者不同的情况我认为应该是一样的,因为是继承的关系都在一条原型链上,只要对象在其上面能找到prototype属性就够了

@Aaaaaaaty Aaaaaaaty changed the title Javascript之bind——折腾记 Javascript之bind Jun 2, 2017
@Aaaaaaaty Aaaaaaaty changed the title Javascript之bind 重温Javascript之bind Jun 2, 2017
@Aaaaaaaty Aaaaaaaty changed the title 重温Javascript之bind Javascript之bind Jun 2, 2017
@xumeiyan
Copy link

xumeiyan commented Jun 2, 2017

了解了。非常感谢。下面还有个fNOP.prototype = this.prototype;

@tobeyouth
Copy link

👍 可以用 https://github.com/Raynos/function-bind 这个库的源码作为讲解,估计会更清晰

@Aaaaaaaty
Copy link
Owner Author

@xumeiyan 这句就是FNOP这个空函数的prototype属性的指向,指向bar的prototype,这样后面new出来的就在这个空函数的原型链上了。instanceof也就是true了

@Aaaaaaaty
Copy link
Owner Author

@tobeyouth 哈哈好的,待会研究一下 不过我一直没太明白bind返回的函数没有prototype,那new出来的实例不就没有原型链了么

@HOUCe
Copy link

HOUCe commented Jun 6, 2017

额。。。我只想说,我被作者的头像惊呆了。。。

@Aaaaaaaty
Copy link
Owner Author

Aaaaaaaty commented Jun 6, 2017

@HOUCe 妹子太惊艳么233, 看了您的文章觉得好棒,还看了下面的评论决定自己再梳理一遍哈哈哈

@HOUCe
Copy link

HOUCe commented Jun 7, 2017

需要BAT内推 欢迎联系我啊。微信:13051310872

@Aaaaaaaty
Copy link
Owner Author

Aaaaaaaty commented Jun 7, 2017

@HOUCe 好的谢谢您

@mqyqingfeng
Copy link

@HOUCe 诚恳的希望你招了这个妹纸~

@Aaaaaaaty
Copy link
Owner Author

@mqyqingfeng 这..这是我女票[捂脸]

@mqyqingfeng
Copy link

@Aaaaaaaty 没有问题呐,接着让 @HOUCe 招你女票,教成前端~ 😂

@Aaaaaaaty
Copy link
Owner Author

@mqyqingfeng 就是不招我哈哈哈哈哈😂

@mqyqingfeng
Copy link

@Aaaaaaaty 哈哈,这个就要看 @HOUCe 啦~

@Aaaaaaaty
Copy link
Owner Author

没关系 慢慢向 @mqyqingfeng @HOUCe 老师们学习~

@mqyqingfeng
Copy link

哎呀,老师这个称呼可不敢当,不过我祝愿你们俩都能被 @HOUCe 老师招走 😀

@Aaaaaaaty
Copy link
Owner Author

@mqyqingfeng 哈哈哈好的谢谢您

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants