标签(空格分隔): 博客 JS


缘由

三面和面试官激情肉搏后发现自己的JS基础实在太差了。接下来的日子决定每天一篇博客,已加强自己对JS的认知,顺便拾起自己糟糕的文笔。希望对看这篇文章的人有所启发

First

TDD 测试驱动开发

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
b

本系列采用mocha进行测试,包含详尽的测试用例

对不懂前端的人来说似乎很复杂,直接来看测试代码

  1. const sinon = require("sinon");
  2. const sinonChai = require("sinon-chai");
  3. chai.use(sinonChai);
  4. const assert = chai.assert;
  5. describe('手写bind',()=>{
  6. Function.prototype.bind2 = newBind
  7. it('这是一个函数',()=>{
  8. assert.isFunction(newBind)
  9. }),
  10. it('this绑定成功',()=>{
  11. const animal={name:'猴子'}
  12. const demo=function () {
  13. return this
  14. }
  15. const test=demo.bind2(animal)
  16. assert(test()===animal)
  17. }),
  18. it('this,p1,p2绑定成功',()=>{
  19. const animal={name:'猴子'}
  20. const demo=function (p1,p2) {
  21. return [this,p1,p2]
  22. }
  23. const test=demo.bind2(animal,'test1','test2')
  24. assert(test()[0]===animal)
  25. assert(test()[1]==='test1')
  26. assert(test()[2]==='test2')
  27. }),
  28. })

接下来开始写代码

第一版

首先newBind肯定要返回一个函数,要将给定的this与参数绑定到新函数上
于是就有了第一版

  1. const newBind = function (asThis, ...arg) {
  2. let fn = this
  3. return function () {
  4. return fn.call(asThis,...arg)
  5. }
  6. }

利用了es6的…操作符直接获取到参数 并且利用call将asThis绑定到fn

第二版

接着继续进行改造,这时有一个bug,第一版的是不支持new的!
写个测试代码来看看

  1. it('支持new', () => {
  2. Function.prototype.bind2 = newBind;
  3. const fn = function (p1, p2) {
  4. this.p1 = p1;
  5. this.p2 = p2;
  6. };
  7. const fn2 = fn.bind(undefined, "x", "y");
  8. const object = new fn2();
  9. assert(object.p1 === "x", "x");
  10. assert(object.p2 === "y", "y");
  11. })

我们先换成bind来试试,fn上bind了undifine即绑定默认this。将x,y绑定到默认this上面,
接着new fn2 可以看到object的p1,p2都与x,y一一对应。但是我们换成我们写的bind2
测试,就发生以下错误
不会吧 不会吧,不会有人不会手写bind吧 - 图1

我们把object打出来,却发现是空对象!!!

为什么是空对象呢? 这就要提到new一个对象的时候到底发生了,详情请见不会吧不会吧之二—-你怎么还不理解new操作?
简而言之,当我们调用bind2的时候返回一个函数。当我们对这个函数进行new操作的时候,一般会发生以:

  • 首先产生一个临时空对象,将空对象的 proto 绑定到要 new 的 对象的原型上,然后返回这个空对象。
    问题就出在我们在定义这个函数的时候已经指定了返回值!!!
  1. return function result(){
  2. return fn.call(asThis,...arg)
  3. }

我们在这里调用了的另外一个This!! 问题是我们tm根本没有给this,我们调用的是undifined!!这就导致了p1,p2绑定到了window对象!

所以我们接下来就要判断到底操作者是用了new还是直接调用

那如何才能判断呢? 来看上文,new一个对象的时候发生了什么,我们把一个临时对象的 proto绑定到了要new 的对象的原型上。即 如果发生了 this.proto ===result.prototype就表示用了new,我们就需要在call的时候直接调用this而非调用asThis
看代码

  1. return function result() {
  2. return fn.call(this.__proto__ === result.prototype ? this : asThis, ...arg)

接下来运行测试用例,很明显通过

还有Bug

如果我们在fn的原型上加了东西呢?

  1. it('支持原型链', () => {
  2. Function.prototype.bind2 = newBind
  3. const fn = function (p1, p2) {
  4. this.p1 = p1;
  5. this.p2 = p2;
  6. };
  7. fn.prototype.sayHi=function () {
  8. }
  9. const fn2 = fn.bind2(undefined, "x", "y");
  10. const object = new fn2();
  11. assert(object.p1 === "x", "x");
  12. assert(object.p2 === "y", "y");
  13. assert.isFunction(object.sayHi)
  14. })

重点看最后一行,new一个对象的时候会继承该对象的原型,即object的原型链上是有sayHi的。那么它肯定是一个函数,运行一下发现,很明显报错。
继续改代码

  1. const newBind = function (asThis, ...arg) {
  2. let fn = this
  3. function resultFn() {
  4. return fn.call(this.__proto__ === resultFn.prototype ? this : asThis, ...arg)
  5. }
  6. resultFn.prototype = this.prototype
  7. return resultFn

我们其实只需要把resultFn的prototype绑定到最初this的prototype上就行了。再测试一下,顺利通过。

继续

还是有Bug!!!
我们这里的bind使用了es6的语法,所以要poyfill

  1. const newBind2 = function (asThis) {
  2. var args = slice.call(arguments, 1)
  3. var fn = this
  4. function resultFn() {
  5. var args2 = slice.call(arguments, 1)
  6. return fn.call(resultFn.prototype.isPrototypeOf(this) ? this : asThis, args.concat(args2))
  7. }
  8. resultFn.prototype = fn.prototype
  9. return resultFn
  10. }

至于为什么就不解释了,自己看