实现一个bind函数

目前的打算还是继续深入前端基础知识,所以打算从polyfill开始做起。

bind函数

bind函数最常见的用法是绑定函数的上下文,比如在setTimeout中的this一般都是指向window,如果我们想改变上下文,这里可以使用bind函数来实现。

var a = 10;
var test = function() {
    console.log(this.a);
}
// 如果直接执行test,最终打印的是10.
var bindTest = test.bind({a: "111"})
bindTest(); // 111
复制代码

从上面这个例子可以看出来,bind函数改变了test函数中this的指向。 除此之外,bind函数还有两个特殊的用法,一个是柯里化,一个是绑定构造函数无效。

柯里化

bind函数的柯里化其实是不完全的,其实只做了一次柯里化,看过MDN的polyfill实现后也就理解了。

var test = function(b) {
    return this.a + b;
}
// 如果直接执行test,最终打印的是10.
var bindTest1 = test.bind({a: 20});
bindTest1(10); // 30
// 这里的bind是个柯里化的函数
var bindTest2 = test.bind({a: 20}, 10);
bindTest2(); // 30;
复制代码

构造函数无效

其实准确的来说,bind并不是对构造函数无效,只是对new的时候无效,如果直接执行构造函数,那么还是有效的。

var a = 10;
var Test = function(a) {
    console.log(this.a);
}
var bindTest = Test.bind({a: 20});
bindTest(); // 20
// 在new的时候,Test中的this并没有指向bind中的对象
new bindTest(); // undefined
复制代码

实现一个bind

我们可以先实现一个简易版本的bind,再不断完善。由于是在函数上调用bind,所以bind方法肯定存在于Function.prototype上面,其次bind函数要有改变上下文的作用,我们想一想,怎么才能改变上下文?没错,就是call和apply方法。

然后还要可以柯里化,还好这里只是简单的柯里化,我们只要在bind中返回一个新的函数,并且将前后两次的参数收集起来就可以做到了。

Function.prototype.bind = function() {
    var args = arguments;
    // 获取到新的上下文
    var context = args[0];
    // 保存当前的函数
    var func = this;
    // 获取其他的参数
    var thisArgs = Array.prototype.slice.call(args, 1);
    var returnFunc = function() {
        // 将两次获取到的参数合并
        Array.prototype.push.apply(thisArgs, arguments)
        // 使用apply改变上下文
        return func.apply(context, thisArgs);
    }
    return returnFunc;
}
复制代码

这里实现了一个简单的bind函数,可以支持简单的柯里化,也可以改变上下文作用域,但是在new一个构造函数的时候还是会改变上下文。

这里我们需要考虑一下,怎么做才能让在new的时候无效,而其他时候有效?

所以我们需要在returnFunc里面的apply第一个参数进行判断,如果是用new调用构造函数的时候应该传入函数本身,否则才应该传入context,那么该怎么判断是new调用呢?

关于在new一个构造函数的时候,这中间做了什么,建议参考这个问题:在js里面当new了一个对象时,这中间发生了什么?

所以我们很容易得出,由于最终返回的是returnFunc,所以最终是new的这个函数,而在new的过程中,会执行一遍这个函数,所以这个过程中returnFunc里面的this指向new的时候创建的那个对象,而那个新对象指向returnFunc函数。

但是我们希望调用后的结果只是new的func函数,和我们正常new func一样,所以这里猜想,在returnFunc中,一定会将其this传入func函数中执行,这样才能满足这几个条件。

Function.prototype.bind = function() {
    var args = arguments || [];
    var context = args[0];
    var func = this;
    var thisArgs = Array.prototype.slice.call(args, 1);
  	var returnFunc = function() {
      Array.prototype.push.apply(thisArgs, arguments);
      // 最关键的一步,this是new returnFunc中创建的那个新对象,此时将其传给func函数,其实相当于做了new操作最后一步(执行构造函数)
      return func.apply(this instanceof returnFunc ? this : context, thisArgs);
    }
    return returnFunc
}
function foo(c) {
    this.b = 100;
    console.log(c);
    return this.a;
}

var func =  foo.bind({a:1});
var newFunc = new func() // undefined
复制代码

但是这样还是不够的,如果foo函数原型上面还有更多的方法和属性,这里的newFunc是没法获取到的,因为foo.prototype不在newFunc的原型链上面。 所以这里我们需要做一些改动,由于传入apply的是returnFunc的一个实例(this),所以我们应该让returnFunc继承func函数,最终版是这样的。

Function.prototype.bind = function() {
    var args = arguments || [];
    var context = args[0];
    var func = this;
    var thisArgs = Array.prototype.slice.call(args, 1);
    var returnFunc = function() {
      Array.prototype.push.apply(thisArgs, arguments);
      // 最关键的一步,this是new returnFunc中创建的那个新对象,此时将其传给func函数,其实相当于做了new操作最后一步(执行构造函数)
      return func.apply(this instanceof func ? this : context, thisArgs);
    }
    returnFunc.prototype = new func()
    return returnFunc
}
复制代码

这样我们就完成了一个bind函数,这与MDN上面的polyfill实现方式大同小异,这里可以参考一下MDN的实现:Function.prototype.bind()

参考链接:

  1. MDN:Function.prototype.bind()

  2. 手写bind()函数,理解MDN上的标准Polyfill

转载于:https://juejin.im/post/5c0cdcb9e51d4504a02eb5bb

<think>我们需要实现一个bind函数bind函数的主要功能是创建一个新的函数,当这个新函数被调用时,其this值会被绑定到bind的第一个参数,其余参数将作为新函数的参数传入。 实现要点: 1. 绑定this(第一个参数) 2. 可以预设参数(部分应用函数) 3. 返回函数可以作为构造函数使用(此时绑定的this失效,但预设参数依然有效) 4. 需要正确处理原型链 步骤: 1. 在Function的原型上实现myBind方法(或者独立函数,但这里我们按方法实现) 2. 保存当前函数(this) 3. 获取绑定的this值(第一个参数)和预设的参数(后面的参数) 4. 创建一个新的函数(bound function) 5. 在新函数内部,判断是否通过new调用(通过instanceof检查) 6. 如果是通过new调用,则使用新创建的this(即实例对象),否则使用绑定的this 7. 将预设参数和调用时传入的参数合并 8. 执行原函数返回结果 9. 处理原型链:将返回函数的原型指向原函数的原型(为了确保通过new创建的对象能继承原函数的原型属性) 10. 注意:我们使用一个函数来中转原型,避免直接修改bound函数的原型影响原函数的原型 代码实现: ```javascript Function.prototype.myBind = function(context, ...args) { const self = this; // 保存原函数 // 定义一个函数用于中转原型 const fNOP = function() {}; // 定义返回的绑定函数 const fBound = function(...innerArgs) { // 如果通过new调用,则this指向实例,否则指向绑定的context return self.apply( this instanceof fBound ? this : context, args.concat(innerArgs) ); }; // 维护原型:将空函数的原型指向原函数的原型 fNOP.prototype = this.prototype; // 将返回函数的原型指向空函数的实例,这样就不会直接修改原函数的原型 fBound.prototype = new fNOP(); return fBound; }; ``` 测试用例: ```javascript // 测试1:基本绑定 const obj = { value: 42 }; function fn() { return this.value; } const boundFn = fn.myBind(obj); console.log(boundFn()); // 42 // 测试2:预设参数 function sum(a, b) { return a + b; } const add10 = sum.myBind(null, 10); console.log(add10(5)); // 15 // 测试3:作为构造函数 function Person(name, age) { this.name = name; this.age = age; } const BoundPerson = Person.myBind(null, 'Alice'); const p = new BoundPerson(25); console.log(p.name, p.age); // Alice 25 console.log(p instanceof Person); // true ``` 注意事项: 1. 使用空函数中转原型是为了避免直接修改fBound.prototype = this.prototype,这样修改fBound.prototype会直接影响原函数的原型。 2. 在判断是否通过new调用时,我们检查this是否是fBound的实例。因为通过new调用时,this会指向新创建的实例,而该实例的原型链指向fBound.prototype,所以this instanceof fBound为true。 如果不想使用中间函数,也可以使用Object.create来创建原型链,但同样需要避免直接修改原函数的原型。 另一种写法(使用Object.create): ```javascript Function.prototype.myBind = function(context, ...args) { const self = this; const bound = function(...innerArgs) { return self.apply( this instanceof bound ? this : context, args.concat(innerArgs) ); }; // 使用Object.create创建新对象,其原型为原函数的原型 bound.prototype = Object.create(this.prototype); // 或者:bound.prototype = Object.create(this.prototype, { constructor: { value: bound } }); 可以设置constructor return bound; }; ``` 注意:使用Object.create可以避免中间函数,但是需要确保原型链正确,并且constructor属性可能需要手动设置(非必须,因为通常我们不会依赖绑定后函数的constructor)。 以上两种方法都可以,第一种方法(空函数中转)在旧环境中更兼容(比如不支持Object.create的浏览器),但第二种更简洁。 根据需求选择,这里我们采用第一种方法(空函数中转)实现。 现在,按照要求,在最后提出相关问题。</think>### 手动实现 `bind` 函数详解 #### 核心实现思路 ```javascript Function.prototype.myBind = function(context, ...args) { // 1. 保存原始函数引用 const originalFunc = this; // 2. 创建绑定函数 const boundFunction = function(...innerArgs) { // 3. 判断是否通过 new 调用 const isNewCall = this instanceof boundFunction; // 4. 确定执行上下文 const executeContext = isNewCall ? this : context; // 5. 执行原始函数返回结果 return originalFunc.apply(executeContext, [...args, ...innerArgs]); }; // 6. 维护原型链 boundFunction.prototype = Object.create(originalFunc.prototype); return boundFunction; }; ``` #### 关键特性实现原理 1. **`this` 绑定机制** ```javascript // 普通调用时 this 指向 context const obj = { name: "Alice" }; function greet() { return this.name; } greet.myBind(obj)(); // "Alice" ``` 2. **参数预设功能** ```javascript function add(a, b) { return a + b; } const addFive = add.myBind(null, 5); addFive(3); // 8 ``` 3. **构造函数支持** ```javascript function Person(name) { this.name = name; } const BoundPerson = Person.myBind(null, "Bob"); const instance = new BoundPerson(); instance.name; // "Bob" (new 调用时 this 指向新实例) ``` 4. **原型链维护** ```javascript Person.prototype.age = 30; console.log(instance.age); // 30 (原型链正常继承) ``` #### 边界情况处理 1. **空上下文处理** ```javascript function demo() { return this === window; } demo.myBind()(); // true (严格模式下为 undefined) ``` 2. **函数属性保留** ```javascript originalFunc.customProp = "value"; boundFunction.customProp; // undefined (需额外处理属性复制) ``` #### 完整实现(含优化) ```javascript Function.prototype.myBind = function(context, ...args) { if (typeof this !== 'function') { throw new TypeError('绑定对象必须是函数'); } const originalFunc = this; const noop = function() {}; // 空函数中转原型 const boundFunction = function(...innerArgs) { // 使用严格模式判断 this const isStrict = (function() { return !this; })(); const effectiveContext = isStrict && !context ? undefined : context; return originalFunc.apply( this instanceof boundFunction ? this : effectiveContext, args.concat(innerArgs) ); }; // 原型链继承优化 if (originalFunc.prototype) { noop.prototype = originalFunc.prototype; boundFunction.prototype = new noop(); noop.prototype = null; // 避免内存泄漏 } // 复制函数属性(可选) Object.defineProperties(boundFunction, { length: { value: Math.max(0, originalFunc.length - args.length) }, name: { value: `bound ${originalFunc.name || ''}` } }); return boundFunction; }; ``` #### 测试用例 ```javascript // 基础绑定测试 const obj = { x: 42 }; function testFn() { return this.x; } console.log(testFn.myBind(obj)()); // 42 // 参数预设测试 function sum(a, b, c) { return a + b + c; } const sum10 = sum.myBind(null, 10); console.log(sum10(20, 30)); // 60 // 构造函数测试 function Animal(name) { this.name = name; } Animal.prototype.speak = function() { return this.name; }; const BoundAnimal = Animal.myBind(null, "Whiskers"); const cat = new BoundAnimal(); console.log(cat.speak()); // "Whiskers" ``` #### 与原生的差异 | 特性 | 原生 `bind` | 手动实现 `myBind` | |------------------|---------------------|---------------------| | 原型链继承 | 完整支持 | 完整支持 | | 函数名 | 添加 "bound "前缀 | 可自定义实现 | | 函数长度 | 正确计算剩余参数 | 需手动计算 | | 严格模式处理 | 自动处理 | 需额外判断 | | 函数属性复制 | 不复制 | 可选实现 |
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值