本文详解如何在手写JavaScript的bind函数时兼容new操作符。通过this instanceof判断调用方式,结合原型链指向与参数预置,实现既支持改变this又支持实例化的完整bind逻辑,并对比了core.js等库的标准实现细节。
各位好,我是前端西瓜哥。
此前我曾撰写一篇关于 JS 中 bind 方法实现的文章,并提供了相应的实现代码:
Function.prototype.myBind = function(thisArg, ...prefixArgs) {
const fn = this;
return function(...args) {
return fn.call(thisArg, ...prefixArgs, ...args);
}
}
然而,该实现并未涵盖通过 new 关键字创建实例的情形。
鉴于实际开发中极少出现对 bind 返回函数执行 new 操作的场景,故未对此特殊情况进行处理。
尽管场景少见,但面试中常会涉及此考点,因此我们需实现兼容 new 操作的 bind 写法,并借此深入理解 new 操作符。
鉴于本文内容存在一定上下文依赖,建议读者在阅读本文前,先阅读前作:《前端面试题:手写 bind》。
new
首先,我们来学习 new 操作符的基本原理。
new 操作用于通过构造函数创建对象实例,这一机制在多种编程语言中均有体现。
JS 中的函数,除了作为普通函数使用外,例如:
function sum(a, b) {
return a + b;
}
还可以作为构造函数使用,只需在调用时于其前添加 new 关键字:
function Person(name, age) {
this.name = name;
this.age = age;
}
const person = new Person('前端西瓜哥', 100)
// Person {name: '前端西瓜哥', age: 100}
new 操作在创建新对象时,执行了以下步骤:
- 创建一个空对象
{}; - 将该空对象的原型属性
__proto__指向构造函数的原型对象Person.prototype; - 将函数内部的 this 指向绑定至该空对象;
- 若函数未显式返回对象,则返回该 this 指向的对象,否则返回显式返回的对象。
判断函数是否通过 new 被调用
如何判断一个函数正被 new 操作符调用?
解决方案是:利用 instanceof 判断 this 是否为当前函数的实例,即 this instanceof Fn 为 true,表明正在通过 new 构建实例。
以下是一个示例:
function Person() {
if (this instanceof Person) {
console.log('通过 new 构建实例');
} else {
console.log('普通调用')
}
}
Person() // 输出:普通调用
new Person() // 输出:通过 new 构建实例
在 Vuejs 源码中,你可以看到如下代码,此处同样运用了这一技巧。
function Vue(options) {
if (__DEV__ && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
若在开发环境中未通过 new 调用 Vue 对象,控制台将会提示你必须使用 new 来调用 Vue。
new 和 bind
若我们对 Function.prototype.bind 返回的新函数执行 new 操作,会发生什么?
function Person(name, age) {
this.name = name;
this.age = age;
}
const BoundPerson = Person.bind(null, '前端西瓜哥');
const boundPerson = new BoundPerson(100);
// Person {name: '前端西瓜哥', age: 100}
boundPerson.__proto__ === Person.prototype
// true
其结果等价于直接对原始函数执行 new 操作。
不同之处在于,仍可进行参数的预置。可见,构造函数的第一个参数在调用 bind 时已预先设置为 '前端西瓜哥'。
实现完整的 bind
完整实现代码如下:
Function.prototype.myBind = function(thisArg, ...prefixArgs) {
const fn = this;
const boundFn = function(...args) {
// 通过 new 使用当前函数
if (this instanceof boundFn) {
return new fn(...prefixArgs, ...args);
}
// 普通的方法调用当前函数
return fn.call(thisArg, ...prefixArgs, ...args);
}
boundFn.prototype = fn.prototype;
return boundFn;
}
此处我通过 this instanceof boundFn 来判断是否使用了 new,若是,则直接 new 原始函数并返回,同时需携带 bind 预置好的参数。
其他部分保持原样(具体细节见上文《前端面试题:手写 bind》)。
boundFn.prototype = fn.prototype; 这一行代码可选,其作用仅是使 bind 返回的新函数的 prototype 指向原函数的 prototype。
原生 bind 返回的函数并无 prototype 属性,可视为一种特殊函数;而我们实现的 bind 返回的是普通函数,因此无法完全模拟原生行为。
若你追求完美的实现,可研读 Function.prototype.bind 的标准规范:
随后查看知名 core.js 库中关于 bind 的实现:
其核心实现逻辑为:
// `Function.prototype.bind` method implementation
// https://tc39.es/ecma262/#sec-function.prototype.bind
module.exports = Function.bind || function bind(that /* , ...args */) {
var F = aCallable(this);
var Prototype = F.prototype;
var partArgs = arraySlice(arguments, 1);
var boundFunction = function bound(/* args... */) {
var args = concat(partArgs, arraySlice(arguments));
return this instanceof boundFunction ? construct(F, args.length, args) : F.apply(that, args);
};
if (isObject(Prototype)) boundFunction.prototype = Prototype;
return boundFunction;
};
此处涉及更多细节:
- 此处判断了 this 是否为函数类型,若非函数则抛出错误;
- F.prototype 需为对象或函数,方可赋值给新函数;
- 使用了普通函数和 arguments 对象,旨在兼容 ES5 环境。
结尾
回顾手写 bind 的过程,看似简单实则复杂,需掌握诸多知识点:
- bind 的详尽用法:涵盖改变 this、预置参数、new 的表现;
- 闭包的使用:用于保存私有变量;
- 通过原型链的方式(
this instanceof boundFn)判断是否通过 new 调用当前函数; - 使用 call 在执行时改变函数的 this 指向。
至此,关于JS手写bind之处理new的情况详解的文章介绍完毕,更多相关JS处理new内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!