sisterAn / JavaScript-Algorithms

基础理论+JS框架应用+实践,从0到1构建整个前端算法体系
5.51k stars 634 forks source link

解析 bind 原理,并手写 bind 实现 #81

Open sisterAn opened 4 years ago

sisterAn commented 4 years ago

bind()

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

— MDN

bind 方法与 call / apply 最大的不同就是前者返回一个绑定上下文的函数,而后两者是直接执行了函数。

来个例子说明下:

let value = 2;
let foo = {
    value: 1
};
function bar(name, age) {
    return {
        value: this.value,
        name: name,
        age: age
    }
};

bar.call(foo, "Jack", 20); // 直接执行了函数
// {value: 1, name: "Jack", age: 20}

let bindFoo1 = bar.bind(foo, "Jack", 20); // 返回一个函数
bindFoo1();
// {value: 1, name: "Jack", age: 20}

let bindFoo2 = bar.bind(foo, "Jack"); // 返回一个函数
bindFoo2(20);
// {value: 1, name: "Jack", age: 20}

通过上述代码可以看出 bind 有如下特性:

模拟实现:

Function.prototype.bind = function (context) {
    // 调用 bind 的不是函数,需要抛出异常
    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    // this 指向调用者
    var self = this;
    // 实现第2点,因为第1个参数是指定的this,所以只截取第1个之后的参数
    var args = Array.prototype.slice.call(arguments, 1); 

    // 实现第3点,返回一个函数
    return function () {
        // 实现第4点,这时的arguments是指bind返回的函数传入的参数
        // 即 return function 的参数
        var bindArgs = Array.prototype.slice.call(arguments);
        // 实现第1点
        return self.apply( context, args.concat(bindArgs) );
    }
}

但还有一个问题,bind 有以下一个特性:

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

来个例子说明下:

let value = 2;
let foo = {
    value: 1
};
function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
bar.prototype.friend = 'kevin';

let bindFoo = bar.bind(foo, 'Jack');
let obj = new bindFoo(20);
// undefined
// Jack
// 20

obj.habit;
// shopping

obj.friend;
// kevin

上面例子中,运行结果 this.value 输出为 undefined ,这不是全局 value 也不是 foo 对象中的 value ,这说明 bind 的 this 对象失效了,new 的实现中生成一个新的对象,这个时候的 this 指向的是 obj

这个可以通过修改返回函数的原型来实现,代码如下:

Function.prototype.bind = function (context) {
    // 调用 bind 的不是函数,需要抛出异常
    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    // this 指向调用者
    var self = this;
    // 实现第2点,因为第1个参数是指定的this,所以只截取第1个之后的参数
    var args = Array.prototype.slice.call(arguments, 1);

    // 创建一个空对象
    var fNOP = function () {};

    // 实现第3点,返回一个函数
    var fBound = function () {
        // 实现第4点,获取 bind 返回函数的参数
        var bindArgs = Array.prototype.slice.call(arguments);
        // 然后同传入参数合并成一个参数数组,并作为 self.apply() 的第二个参数
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
        // 注释1
    }

    // 注释2
    // 空对象的原型指向绑定函数的原型
    fNOP.prototype = this.prototype;
    // 空对象的实例赋值给 fBound.prototype
    fBound.prototype = new fNOP();
    return fBound;
}

注释1

注释2

注意: bind() 函数在 ES5 才被加入,所以并不是所有浏览器都支持,IE8 及以下的版本中不被支持,如果需要兼容可以使用 Polyfill 来实现

详情可前往 深度解析bind原理、使用场景及模拟实现 查看

补充:柯里化

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。

var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12

add(1)(2);
// 3

这里定义了一个 add 函数,它接受一个参数并返回一个新的函数。调用 add 之后,返回的函数就通过闭包的方式记住了 add 的第一个参数。所以说 bind 本身也是闭包的一种使用场景。

柯里化是将 f(a,b,c) 可以被以 f(a)(b)(c) 的形式被调用的转化。JavaScript 实现版本通常保留函数被正常调用和在参数数量不够的情况下返回偏函数这两个特性。

Moonquakesss commented 3 years ago

用apply实现bind??

bjw1234 commented 3 years ago

面试的时候这样写会有什么问题吗?🙃

Function.prototype.myBind = function(context, ...args1) {
  if(typeof this !== 'function') {
    throw Error('error')
  }
  context = context ? Object(context) : Window

  const currentFunc = this
  return function(...args2) {
    if(new.target) {
      return new currentFunc(...[...args1, ...args2])
    }
    return currentFunc.apply(context, [...args1, ...args2])
  }
}
Yuanbaby0077 commented 3 years ago

我也是直接返回的new self

Function.prototype._bind = function(ctx) {
  const beforeArgs = [...arguments].slice(1)
  const self = this
  const fn = function() {
    const fullArgs = beforeArgs.concat([...arguments])
    // this是fn的实例,说明是new出来的
    if (this instanceof fn) {
      return new self(...fullArgs) // 这里有什么问题吗??
    }
    return self.apply(ctx, [...fullArgs])
  }
  return fn
}
Anbelin commented 3 years ago
let value = 2;
let foo = {
    value: 1
};
function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
bar.prototype.friend = 'kevin';

let bindFoo = bar.bind(foo, 'Jack');
let obj = new bindFoo(20);
// undefined
// Jack
// 20

obj.habit;
// shopping

obj.friend;
// kevin

这个我运行出来不是 // undefined // Jack // 20 而是 // 1 // Jack // 20 为啥啊? 2021-08-20_170944 2021-08-20_170959

Hhpon commented 3 years ago
let value = 2;
let foo = {
    value: 1
};
function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
bar.prototype.friend = 'kevin';

let bindFoo = bar.bind(foo, 'Jack');
let obj = new bindFoo(20);
// undefined
// Jack
// 20

obj.habit;
// shopping

obj.friend;
// kevin

这个我运行出来不是 // undefined // Jack // 20 而是 // 1 // Jack // 20 为啥啊? 2021-08-20_170944 2021-08-20_170959

你这个也没有考虑bind生成的函数在使用new关键字调用的情况呀 我们在使用new关键字调用方法bindFoo的时候,传入的context是不起作用的,我们可以直接忽视 我们可以通过这个方法判断是否是使用new关键字调用的“myBind方法返回匿名函数当中的this是指向这个匿名函数的”

xllpiupiu commented 3 years ago
Function.prototype.myBind = function (obj) {
    if (typeof this !== 'function') {
        throw new TypeError('bind is only work for Function')
    }
    let first = Array.prototype.slice.call(arguments, 1);//因为第一个参数是obj
    let fn_ = function () { };
    let fn = this;//调用bind的函数
    fn_.prototype = fn.prototype;
    const bindFn = function () {
        let second = Array.prototype.slice.call(arguments);
        fn.apply(this.constructor === fn ? this : obj, first.concat(second));
        console.log(this);
    }
    bindFn.prototype = new fn_();
    return bindFn;
}
yafeng-c commented 2 years ago

我不理解,修改后的版本,this.value不还是输出undefined吗...

Lanchouchou commented 2 years ago

我不理解,修改后的版本,this.value不还是输出undefined吗...

返回undefined 才是对的,修改之前的版本返回的是1,使用new 调用 this 应该是指向新创建的对象 undefined 才是对的~

jingaier commented 2 years ago

不理解,this为何是bar函数

jingaier commented 2 years ago

这是来自QQ邮箱的假期自动回复邮件。   您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。

roberthuang123 commented 2 years ago

return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs)); 这句写错了啊,正确的应该是: return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));

yysfwls commented 2 years ago

if(new.target)是什么意思哇?

MinimalistYing commented 2 years ago

@yysfwls 判断函数是否通过 new 调用。

jingaier commented 2 years ago

这是来自QQ邮箱的假期自动回复邮件。   您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。

XiangBo-EvanZuo commented 2 years ago

面试的时候这样写会有什么问题吗?🙃

Function.prototype.myBind = function(context, ...args1) {
  if(typeof this !== 'function') {
    throw Error('error')
  }
  context = context ? Object(context) : Window

  const currentFunc = this
  return function(...args2) {
    if(new.target) {
      return new currentFunc(...[...args1, ...args2])
    }
    return currentFunc.apply(context, [...args1, ...args2])
  }
}

ES6的new.target语法,取巧了

ghost commented 2 years ago
Function.prototype._bind = function(t){
 let self = this;
 let a =[].slice.call(arguments,1)    
    return function(){

       self.apply(t,a.concat(arguments))
    }
}
m5ultra commented 1 year ago
Function.prototype.myBind = function (ctx) {
  // 调用 bind 的不是函数,需要抛出异常
  if (typeof this !== "function") {
    throw new Error("ctx must be function");
  }
  const argv1 = Array.prototype.slice.call(arguments, 1)
  const self = this
  return function _bind() {
    const argv2 = Array.prototype.slice.call(arguments)
    const argv = argv1.concat(argv2)

    if (Object.getPrototypeOf(this) === _bind.prototype) {
      // return new self(...argv)
      var obj = Object.assign({}, ctx)
      Object.setPrototypeOf(obj, self.prototype)
      self.apply(obj, argv)
      return obj
    } else {
      return self.apply(ctx, argv)
    }
  }
}

// 测试1
function test(a, b, c, d) {
  console.log(a, b, c, d, 'test')
  return 'x'
}

const module = {
  x: 42,
  getX: function () {
    return this.x
  },
}

const bindTestFn = test.myBind(module, 1, 2)
bindTestFn(3, 4)

// 测试2
let value = 2;
let foo = {
  value: 1
};

function bar(name, age) {
  this.habit = 'shopping';
  console.log(this.value);
  console.log(name);
  console.log(age);
}

bar.prototype.friend = 'kevin';

const bindFoo = bar.myBind(foo, 'Jack')
let obj = new bindFoo(20);
console.log(obj, obj.habit, obj.friend)

jingaier commented 1 year ago

这是来自QQ邮箱的假期自动回复邮件。   您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。