当前位置:网站首页>JS 从零手写实现一个bind方法
JS 从零手写实现一个bind方法
2022-08-10 14:43:00 【行星飞行】
壹 * 引
在 JS 从零手写实现一个call、apply方法 一文中,我们详细分析并模拟实现了call/apply
方法,由于篇幅问题,关于bind
方法实现只能另起一篇。
在模拟bind
之前,我们先了解bind
的概念,这里引入MDN解释:
bind()
方法创建一个新的函数,在 bind()
被调用时,这个新函数的 this
被指定为 bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
说的通俗一点,bind
与apply/call
一样都能改变函数this
指向,但bind
并不会立即执行函数,而是返回一个绑定了this
的新函数,你需要再次调用此函数才能达到最终执行。
我们来看一个简单的例子:
var obj = {
z: 1
};
var obj1 = {
z: 2
};
function fn(x, y) {
console.log(x + y + this.z);
};
// call与apply
fn.call(obj, 2, 3); //6
fn.apply(obj, [2, 3]); //6
var bound = fn.bind(obj, 2);
bound(3); //6
//尝试修改bind返回函数的this
bound.call(obj1, 3); //6
可以到bind
并不是立即执行,而是返回一个新函数,且新函数的this
无法再次被修改,我们总结bind
的特点:
- 可以修改函数
this
指向。 bind
返回一个绑定了this
的新函数boundFunction
,例子中我们用bound
表示。- 支持函数柯里化,我们在返回
bound
函数时已传递了部分参数2,在调用时bound
补全了剩余参数。 boundFunction
的this无法再被修改,使用call、apply
也不行。
考虑到有同学对于柯里化的陌生,这里简单解释,所谓函数柯里化其实就是在函数调用时只传递一部分参数进行调用,函数会返回一个新函数去处理剩下的参数,一个经典简单的例子:
//函数柯里化
function fn(x, y) {
return function (y) {
console.log(x + y);
};
};
var fn_ = fn(1);
fn_(1); //2
fn(1)(1) //2
不难发现函数柯里化使用了闭包,在执行内层函数时,它使用了外层函数的局部形参x,从而构成了闭包,扯远了点。关于函数柯里化具体可见:从函数柯里化聊到add(1)(2)(3) add(1, 2)(3),以及柯里化无限调用
我们来尝试实现bind
方法,先从简单的改变this
和返回函数开始。
贰 * 实现bind
之前已经有了模拟call/apply
的经验,这里直接给出版本一:
Function.prototype.bind_ = function (obj) {
var fn = this;
return function () {
fn.apply(obj);
};
};
var obj = {
z: 1
};
function fn() {
console.log(this.z);
};
var bound = fn.bind_(obj);
bound(); //1
唯一需要留意的就是var fn = this
这一行,如果不提前保存,在执行bound
时内部this
会指向window
。
版本一以满足了this
修改与函数返回,马上有同学就想到了,版本一不支持函数传参,那么我们进行简单修改让其支持传参:
Function.prototype.bind_ = function (obj) {
//第0位是this,所以得从第一位开始裁剪
var args = Array.prototype.slice.call(arguments, 1);
var fn = this;
return function () {
fn.apply(obj, args);
};
};
完美了吗?并不完美,别忘了我们前面说bind
支持函数柯里化,在调用bind
时可以先传递部分参数,在调用返回的bound
时可以补全剩余参数,所以还得进一步处理,来看看bind_
第二版:
Function.prototype.bind_ = function (obj) {
//第0位是this,所以得从第一位开始裁剪
var args = Array.prototype.slice.call(arguments, 1);
var fn = this;
return function () {
//二次调用我们也抓取arguments对象
var params = Array.prototype.slice.call(arguments);
//注意concat的顺序
fn.apply(obj, args.concat(params));
};
};
var obj = {
z: 1
};
function fn(x, y) {
console.log(x + y + this.z);
};
var bound = fn.bind_(obj, 1);
bound(2); //4
看,改变this
,返回函数,函数柯里化均已实现。这段代码需要注意的是args.concat(params)
的顺序,args
在前,因为只有这样才能让先传递的参数和fn
的形参按顺序对应。
至少走到这一步都挺顺序,需要注意的是,bind
方法还有一个少见的特性,这里引用MDN的描述
绑定函数也可以使用 new
运算符构造,它会表现为目标函数已经被构建完毕了似的。提供的 this
值会被忽略,但前置参数仍会提供给模拟函数。
说通俗点,通过bind
返回的boundFunction
函数也能通过new
运算符构造,只是在构造过程中,boundFunction
已经确定的this
会被忽略,且返回的实例还是会继承构造函数的构造器属性与原型属性,并且能正常接收参数。
有点绕口,我们来看个简单的例子:
var z = 0;
var obj = {
z: 1
};
function fn(x, y) {
this.name = '听风是风';
console.log(this.z);
console.log(x);
console.log(y);
};
fn.prototype.age = 26;
var bound = fn.bind(obj, 2);
var person = new bound(3);//undefined 2 3
console.log(person.name);//听风是风
console.log(person.age);//26
在此例子中,我们先是将函数fn
的this
指向了对象obj
,从而得到了bound
函数。紧接着使用new
操作符构造了bound
函数,得到了实例person
。不难发现,除了先前绑定好的this
丢失了(后面会解释原因),构造器属性this.name
,以及原型属性fn.prototype.age
都有顺利继承,除此之外,两个形参也成功传递进了函数。
难点来了,至少在ES6
之前,JavaScript
并没有class类的概念,所谓构造函数其实只是对于类的模拟;而这就造成了一个问题,所有的构造函数除了可以使用new
构造调用以外,它还能被普通调用,比如上面例子中的bound
我们也可以普通调用:
bound(3); //1 2 3
有同学在这可能就有疑惑,bound()
等同于window.bound()
,此时this
不是应该指向window
从而输出0吗?我们在前面说bind
属于硬绑定,一次绑定终生受益,上面的调用本质上等同于:
window.fn.bind(obj, 2);
函数fn
存在this
默认绑定window
与显示绑定bind
,而显示绑定优先级高于默认绑定,所以this
还是指向obj
。关于this优先级以及this指向若有疑问,可见五种绑定彻底弄懂this,默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定详解。
当构造函数被new
构造调用时,本质上构造函数中会创建一个实例对象,函数内部的this
指向此实例,当执行到console.log(this.z)
这一行时,this
上并未被赋予属性z
,所以输出undefined
,这也解释了为什么bound
函数被new
构造时会丢失原本绑定的this
。
是不是觉得ES5
构造函数特别混乱,不同调用方式函数内部this
指向还不同,也正因如此在ES6
中推出了class
类,凡是通过class
创建的类均只能使用new
调用,普通调用一律报错处理:
class Fn {
constructor(name, age) {
this.name = name;
this.age = age;
};
sayName() {
console.log(this.name);
};
};
//只能new构造调用
const person = new Fn('听风是风', 26);
person.sayName(); //听风是风
const person1 = Fn(); //Class constructor Fn cannot be invoked without 'new'
扯远了,让我们回到上面的例子,说了这么多无非是为了强调一点,我们在模拟bind
方法时,返回的bound
函数在调用时得考虑new
调用与普通调用,毕竟两者this
指向不同。
再说直白一点,如果是new
调用,bound
函数中的this
指向实例自身,而如果是普通调用this
指向obj
,怎么区分呢?
不难,我们知道(强行让你们知道)构造函数实例的constructor
属性永远指向构造函数本身(这句话其实有歧义,具体我会在原型的文章中解释),比如:
function Fn(){
};
var o = new Fn();
console.log(o.constructor === Fn);//true
而构造函数在运行时,函数内部this
指向实例,所以this
的constructor
也指向构造函数:
function Fn() {
console.log(this.constructor === Fn); //true
};
var o = new Fn();
console.log(o.constructor === Fn); //true
所以我就用constructor
属性来判断当前bound
方法调用方式,毕竟只要是new
调用,this.constructor === Fn
一定为true
。
让我们简单改写bind_
方法,为bound
方法新增this
判断以及原型继承:
Function.prototype.bind_ = function (obj) {
var args = Array.prototype.slice.call(arguments, 1);
var fn = this;
var bound = function () {
var params = Array.prototype.slice.call(arguments);
//通过constructor判断调用方式,为true this指向实例,否则为obj
fn.apply(this.constructor === fn ? this : obj, args.concat(params));
};
//原型链继承
bound.prototype = fn.prototype;
return bound;
};
有同学就问了,难道不应该是this.constructor===bound
吗?并不是,虽然new
的是bound
方法,本质上执行的还是fn
,毕竟bound
自身并没有构造器属性,这点关系还是需要理清。
其次还有个缺陷。虽然构造函数产生的实例都是独立的存在,实例继承而来的构造器属性随便你怎么修改都不会影响构造函数本身:
function Fn() {
this.name = '听风是风';
this.sayAge = function () {
console.log(this.age);
};
};
Fn.prototype.age = 26;
var o = new Fn();
o.sayAge(); //26
//我们改变实例继承的构造器属性,并不会影响构造函数本身
o.name = 'echo';
var o1 = new Fn();
console.log(o1.name) //听风是风
但是如果我们直接修改实例原型,这就会对构造函数Fn
产生影响,来看个例子:
function Fn() {
this.name = '听风是风';
this.sayAge = function () {
console.log(this.age);
};
};
Fn.prototype.age = 26;
var o = new Fn();
o.sayAge(); //26
//修改实例的原型属性,这会影响构造函数本身
o.__proto__.age = 18;
var o1 = new Fn();
console.log(o1.age) //18
不难理解,构造器属性(this.name,this.sayAge
)在创建实例时,我们可以抽象的理解成实例深拷贝了一份,这是属于实例自身的属性,后面再改都与构造函数不相关。而实例要用prototype
属性时都是顺着原型链往上找,构造函数有便借给实例用了,一共就这一份,谁要是改了那就都得变。
我们可以输出实例o
,观察它的属性,可以看到age
属性确实是绑原型__proto__
上(注意,prototype
是函数特有,普通对象只有__proto__
,两者指向相同)。
怎么做才保险呢,这里就可以借助一个空白函数作为中介,直接看个例子:
function Fn() {
this.name = '听风是风';
this.sayAge = function () {
console.log(this.age);
};
};
Fn.prototype.age = 26;
// 创建一个空白函数Fn1,单纯的拷贝Fn的prototype
var Fn1 = function () {
};
Fn1.prototype = Fn.prototype;
// 这里的Fn2对应我们的bound方法,将其原型指向Fn1创建的实例
var Fn2 = function () {
};
Fn2.prototype = new Fn1();
var o = new Fn2();
console.log(o.age); //26
//尝试修改
o.__proto__.age = 18;
var o1 = new Fn();
console.log(o1.age);//26
说到底,我们就是借用空白函数,让Fn2的实例多了一层__proto__
,达到修改原型不会影响Fn
原型的目的,当然你如果通过__proto__.__proto__
还是一样能修改,差不多就是这个意思:
o.__proto__.__proto__.age = 18;
var o1 = new Fn();
console.log(o1.age);//18
所以综上,我们再次修改bind_
方法,拿出第四版:
Function.prototype.bind_ = function (obj) {
var args = Array.prototype.slice.call(arguments, 1);
var fn = this;
//创建中介函数
var fn_ = function () {
};
var bound = function () {
var params = Array.prototype.slice.call(arguments);
//通过constructor判断调用方式,为true this指向实例,否则为obj
fn.apply(this.constructor === fn ? this : obj, args.concat(params));
console.log(this);
};
fn_.prototype = fn.prototype;
bound.prototype = new fn_();
return bound;
};
最后,bind
方法如果被非函数调用时会抛出错误,所以我们要在第一次执行bind_
时做一次调用判断,加个条件判断,我们来一个完整的最终版:
Function.prototype.bind_ = function (obj) {
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
};
var args = Array.prototype.slice.call(arguments, 1);
var fn = this;
//创建中介函数
var fn_ = function () {
};
var bound = function () {
var params = Array.prototype.slice.call(arguments);
//通过constructor判断调用方式,为true this指向实例,否则为obj
fn.apply(this.constructor === fn ? this : obj, args.concat(params));
console.log(this);
};
fn_.prototype = fn.prototype;
bound.prototype = new fn_();
return bound;
};
var z = 0;
var obj = {
z: 1
};
function fn(x, y) {
this.name = '听风是风';
console.log(this.z);
console.log(x);
console.log(y);
};
fn.prototype.age = 26;
var bound = fn.bind_(obj, 2);
var person = new bound(3); //undefined 2 3
console.log(person.name); //听风是风
console.log(person.age); //26
person.__proto__.age = 18;
var person = new fn();
console.log(person.age); //26
看着有些长,不过我们顺着思路一步步走过来其实不难理解。
好啦,关于bind
方法的模拟实现就说到这里了,万万没想到这篇实现居然用了我五个小时时间…
另外,如果大家对于new
一个构造函数发生了什么存在疑惑,可以阅读博主这篇文章:
若对于原型理解有所欠缺,可以阅读博主这篇文章:
JS 疫情宅在家,学习不能停,七千字长文助你彻底弄懂原型与原型链
叁 * 参考
边栏推荐
- BFT机器人带你走进智慧生活 ——探索遨博机器人i系列的多种应用
- redhat替换yum源时redhat.repo无法删除或无法禁用的问题解决方法
- 网络安全(加密技术、数字签名、证书)
- 从洞察到决策,一文解读标签画像体系建设方法论
- 1W word detailed thread local storage ThreadLocal
- 【数仓设计】企业数仓为什么要进行分层?(六大好处)
- The a-modal in the antd component is set to a fixed height, and the content is scrolled and displayed
- 从全球价值链视角看,京东云数智供应链对未来经济有何影响?
- file system design
- 容器化 | 在 S3 实现定时备份
猜你喜欢
随机推荐
PyTorch multi-machine multi-card training: DDP combat and skills
file system design
兆骑科创高层次人才创业大赛平台,投融资对接,双创服务
静态变量存储在哪个区
解题-->在线OJ(十九)
网络初识(二)
中学数学建模书籍及相关的视频等(2022.08.09)
Summary of tensorflow installation stepping on the pit
640. 求解方程 : 简单模拟题
Classifying irises using decision trees
王学岗—————————哔哩哔哩直播-手写哔哩哔哩硬编码录屏推流(硬编)(26节课)
物资采购小程序开发制作功能介绍
MySQL Principle and Optimization: Update Optimization
PyTorch 多机多卡训练:DDP 实战与技巧
数字藏品平台系统开发实战
pm2 static file service
符合信创要求的堡垒机有哪些?支持哪些系统?
The a-modal in the antd component is set to a fixed height, and the content is scrolled and displayed
Alibaba的秒杀系统—千亿级并发设计手册上线了
小程序-语音播报功能