js 手动实现bind方法,超详细思路分析!

壹 ❀ 引
在 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的新函数
boundFcuntion,例子中我们用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,从而构成了闭包,扯远了点。
我们来尝试实现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。
当构造函数被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 手动实现bind方法,超详细思路分析!的更多相关文章
- JS中的bind方法
# bind的机制 ``` var foo = function(){} var bar = foo; console.log(foo === bar) //true /--------------- ...
- (企业面试部分)超详细思路讲解SQL语句的查询实现,及数据的创建。
企业面试部分详细的SQL问题,思路讲解 第一步:创建数据库表,及插入数据信息 --Student(S#,Sname,Sage,Ssex) 学生表 CREATE TABLE student( sno ) ...
- JS中的bind方法学习
EcmaScript5给Function扩展了一个方法:bind 众所周知 在jQuery和prototype.js之类的框架里都有个bind jQuery里的用途是给元素绑定事件 $("# ...
- 超详细思路讲解SQL语句的查询实现,及数据的创建。
最近一直在看数据库方面的问题,总结了一下SQL语句,这是部分详细的SQL问题,思路讲解: 第一步:创建数据库表,及插入数据信息 --Student(S#,Sname,Sage,Ssex) 学生表 CR ...
- 分享一个js中的bind方法使用
来源:http://www.cnblogs.com/yuzhongwusan/archive/2012/02/13/2348782.html Js代码 复制代码 代码如下: var first_obj ...
- Spring Boot 日志配置方法(超详细)
默认日志 Logback : 默认情况下,Spring Boot会用Logback来记录日志,并用INFO级别输出到控制台.在运行应用程序和其他例子时,你应该已经看到很多INFO级别的日志了. 从上图 ...
- node.js安装及环境配置超详细教程【Windows系统安装包方式】
文章目录 Step1:下载安装包 Step2:安装程序 Step3:查看 Step4:环境配置 最后补充: Step1:下载安装包 https://nodejs.org/zh-cn/download/ ...
- 八皇后问题 递归实现 C语言 超详细 思路 基础
八皇后问题 :假设 將八个皇后放到国际象棋盘上,使其两两之间无法相互攻击.共有几种摆法? 基础知识: 国际象棋里,棋盘为8X8格. 皇后每步可以沿直线.斜线 走任意格. 思路: 1.想把8个皇后放进去 ...
- VS2015ASP.NET MVC5项目中Spring.NET配置方法(超详细)
首先,在ASP.NET MVC5项目右键,如下图所示,选择“管理Nuget程序包...” 然后,在弹出的页面的搜索框中输入“spring.web”,在返回结果中选择Spring.Web和Spring. ...
- Windows 2016 无域故障转移群集部署方法 超详细图文教程 (二)
上一章我们配置了一台设备,接着根据那个配置,配置其它设备.这里我配置了三台设备: 创建故障转移群集,并添加设备. 之前的操作都是每台服务器都要做的,而这个操作,只需要任选一台去做即可,我这里选d1 1 ...
随机推荐
- 以太网链路连接 和 ISIS/OSPF等路由协议关系
转载请注明出处: 以太网链路连接和ISIS/OSPF协议之间存在关联和区别 关联: 以太网链路连接是指通过以太网物理媒介(如电缆)将网络设备进行连接,使它们可以交换数据. ISIS(Intermedi ...
- Ribbon 负载均衡源码解读
转载请注明出处: 1.什么是Ribbon 是 Netflix 发布的一个负载均衡器,有助于控制 HTTP 和 TCP客户端行为.在 SpringCloud 中, nacos一般配合Ribbon进行使用 ...
- 【MLA】一种内存泄漏分析方法
项目地址:skullboyer/MLA (github.com) 介绍 MLA 即 Memory Leak Analyzer,是一个排查内存泄漏的分析器 实现机制是在malloc时记录分配位置信息,在 ...
- CSS - checkbox 样式
.checkbox-wrap{ position:relative } .checkbox-wrap::before{ content: ''; position: absolute; top: 31 ...
- Go-获取文件MD5值
获取文件的MD5值 crypto/md5 encoding/hex package filetools import ( "crypto/md5" "encoding/h ...
- Blazor SSR/WASM IDS/OIDC 单点登录授权实例5 - Winform 端授权
目录: OpenID 与 OAuth2 基础知识 Blazor wasm Google 登录 Blazor wasm Gitee 码云登录 Blazor SSR/WASM IDS/OIDC 单点登录授 ...
- Oracle19c on 银河麒麟的安装与升级
Oracle19c on 银河麒麟的安装与升级 下载内容 preinstall CentOS8 https://yum.oracle.com/repo/OracleLinux/OL8/appstrea ...
- [转帖]AMD处理器ZEN一代之国产化海光
https://huataihuang.gitbook.io/cloud-atlas-draft/os/linux/kernel/cpu/amd_hygon 2020年国产化处理器受到了广泛的关注 ...
- [转帖]自动化配置SSH免密登录和取消SSH免密配置脚本
1. 前文 搭建了一套有多台主机的局域网环境,不完全考虑安全性的情况下,为方便管理局域网内主机,配置SSH免密登录,因主机较多,前阵子针对配置ssh免密和取消ssh免密功能单独写了脚本来自动化批量部署 ...
- 使用smem 计算Oracle用户下内存占用情况.
1. 本来计划使用 ps 命令进行查看, 但是发现ps 最多查询出来的是 RSS的内存数据, 会多添加很多冗余的内存数据量进来, 于是作罢 2. 找了下公司的方神, 说可以使用smem 然后简单看了下 ...