【进阶3-4期】深度解析bind原理、使用场景及模拟实现(转)
这是我在公众号(高级前端进阶)看到的文章,现在做笔记 https://github.com/yygmind/blog/issues/23
bind()
bind()
方法会创建一个新函数,当这个新函数被调用时,它的this
值是传递给bind()
的第一个参数,传入bind方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。bind返回的绑定函数也能使用new
操作符创建对象:这种行为就像把原函数当成构造器,提供的this
值被忽略,同时调用时的参数被提供给模拟函数。
语法:fun.bind(thisArg[, arg1[, arg2[, ...]]])
bind
方法与 call / apply
最大的不同就是前者返回一个绑定上下文的函数,而后两者是直接执行了函数。
来个例子说明下:
<script>
var value = ; var foo = {
value:
}; function bar(name, age) {
return {
value: this.value,
name: name,
age: age
}
}; console.log(bar())
console.log(bar.call(foo))
var bindfoo1 = bar.bind(foo, "jack", )
console.log(bindfoo1())
var bindfoo2 = bar.bind(foo, "jack1")
console.log(bindfoo2())
</script>
通过上述代码可以看出bind
有如下特性:
- 1、可以指定
this
- 2、返回一个函数
- 3、可以传入参数
- 4、柯里化
使用场景
1、业务场景
经常有如下的业务场景
var nickname = "Kitty";
function Person(name){
this.nickname = name;
this.distractedGreeting = function() { setTimeout(function(){
console.log("Hello, my name is " + this.nickname);
}, );
}
} var person = new Person('jawil');
person.distractedGreeting();
//Hello, my name is Kitty
这里输出的nickname
是全局的,并不是我们创建 person
时传入的参数,因为 setTimeout
在全局环境中执行(不理解的查看【进阶3-1期】),所以 this
指向的是window
。
这边把 setTimeout
换成异步回调也是一样的,比如接口请求回调。
解决方案有下面两种。
解决方案1:缓存 this
值
var nickname = "Kitty";
function Person(name){
this.nickname = name;
this.distractedGreeting = function() { var self = this; // added
setTimeout(function(){
console.log("Hello, my name is " + self.nickname); // changed
}, );
}
} var person = new Person('jawil');
person.distractedGreeting();
// Hello, my name is jawil
解决方案2:使用 bind
var nickname = "Kitty";
function Person(name){
this.nickname = name;
this.distractedGreeting = function() { setTimeout(function(){
console.log("Hello, my name is " + this.nickname);
}.bind(this), );
}
} var person = new Person('jawil');
person.distractedGreeting();
// Hello, my name is jawil
2、验证是否是数组
【进阶3-3期】介绍了 call
的使用场景,这里重新回顾下。
function isArray(obj){
return Object.prototype.toString.call(obj) === '[object Array]';
}
isArray([, , ]);
// true // 直接使用 toString()
[, , ].toString(); // "1,2,3"
"".toString(); // "123"
.toString(); // SyntaxError: Invalid or unexpected token
Number().toString(); // "123"
Object().toString(); // "123"
可以通过toString()
来获取每个对象的类型,但是不同对象的 toString()
有不同的实现,所以通过 Object.prototype.toString()
来检测,需要以 call() / apply()
的形式来调用,传递要检查的对象作为第一个参数。
另一个验证是否是数组的方法,这个方案的优点是可以直接使用改造后的 toStr
。
var toStr = Function.prototype.call.bind(Object.prototype.toString);
function isArray(obj){
return toStr(obj) === '[object Array]';
}
isArray([, , ]);
// true // 使用改造后的 toStr
toStr([, , ]); // "[object Array]"
toStr(""); // "[object String]"
toStr(); // "[object Number]"
toStr(Object()); // "[object Number]"
上面方法首先使用 Function.prototype.call
函数指定一个 this
值,然后 .bind
返回一个新的函数,始终将 Object.prototype.toString
设置为传入参数。其实等价于 Object.prototype.toString.call()
。
这里有一个前提是toString()
方法没有被覆盖
Object.prototype.toString = function() {
return '';
}
isArray([, , ]);
// false
3、柯里化(curry)
只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
可以一次性地调用柯里化函数,也可以每次只传一个参数分多次调用。
var add = function(x) {
return function(y) {
return x + y;
};
}; var increment = add();
var addTen = add(); increment();
// addTen();
// add()();
//
这里定义了一个 add
函数,它接受一个参数并返回一个新的函数。调用 add
之后,返回的函数就通过闭包的方式记住了 add
的第一个参数。所以说 bind
本身也是闭包的一种使用场景。
模拟实现
bind()
函数在 ES5 才被加入,所以并不是所有浏览器都支持,IE8
及以下的版本中不被支持,如果需要兼容可以使用 Polyfill 来实现。
首先我们来实现以下四点特性:
- 1、可以指定
this
- 2、返回一个函数
- 3、可以传入参数
- 4、柯里化
模拟实现第一步
对于第 1 点,使用 call / apply
指定 this
。
对于第 2 点,使用 return
返回一个函数。
结合前面 2 点,可以写出第一版,代码如下:
// 第一版
Function.prototype.bind2 = function(context) {
var self = this; // this 指向调用者
return function () { // 实现第 2点
return self.apply(context); // 实现第 1 点
}
}
测试一下
// 测试用例
var value = ;
var foo = {
value:
}; function bar() {
return this.value;
} var bindFoo = bar.bind2(foo); bindFoo(); //
模拟实现第二步
对于第 3 点,使用 arguments
获取参数数组并作为 self.apply()
的第二个参数。
对于第 4 点,获取返回函数的参数,然后同第3点的参数合并成一个参数数组,并作为 self.apply()
的第二个参数。
// 第二版
Function.prototype.bind2 = function (context) { var self = this;
// 实现第3点,因为第1个参数是指定的this,所以只截取第1个之后的参数
// arr.slice(begin); 即 [begin, end]
var args = Array.prototype.slice.call(arguments, ); return function () {
// 实现第4点,这时的arguments是指bind返回的函数传入的参数
// 即 return function 的参数
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply( context, args.concat(bindArgs) );
}
}
测试一下:
// 测试用例
var value = ; var foo = {
value:
}; function bar(name, age) {
return {
value: this.value,
name: name,
age: age
}
}; var bindFoo = bar.bind2(foo, "Jack");
bindFoo();
// {value: 1, name: "Jack", age: 20}
模拟实现第三步
到现在已经完成大部分了,但是还有一个难点,bind
有以下一个特性
一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
来个例子说明下:
// 第三版
Function.prototype.bind2 = function (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, ); var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments); // 注释1
return self.apply(
this instanceof fBound ? this : context,
args.concat(bindArgs)
);
}
// 注释2
fBound.prototype = this.prototype;
return fBound;
}
- 注释1:
- 当作为构造函数时,this 指向实例,此时
this instanceof fBound
结果为true
,可以让实例获得来自绑定函数的值,即上例中实例会具有habit
属性。 - 当作为普通函数时,this 指向
window
,此时结果为false
,将绑定函数的 this 指向context
- 当作为构造函数时,this 指向实例,此时
- 注释2: 修改返回函数的
prototype
为绑定函数的prototype
,实例就可以继承绑定函数的原型中的值,即上例中obj
可以获取到bar
原型上的friend
。
注意:这边涉及到了原型、原型链和继承的知识点,可以看下我之前的文章。
模拟实现第四步
上面实现中 fBound.prototype = this.prototype
有一个缺点,直接修改 fBound.prototype
的时候,也会直接修改 this.prototype
。
来个代码测试下:
// 测试用例
var value = ;
var foo = {
value:
};
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'kevin'; var bindFoo = bar.bind2(foo, 'Jack'); // bind2
var obj = new bindFoo(); // 返回正确
// undefined
// Jack
// obj.habit; // 返回正确
// shopping obj.friend; // 返回正确
// kevin obj.__proto__.friend = "Kitty"; // 修改原型 bar.prototype.friend; // 返回错误,这里被修改了
// Kitty
解决方案是用一个空对象作为中介,把 fBound.prototype
赋值为空对象的实例(原型式继承)。
var fNOP = function () {}; // 创建一个空对象
fNOP.prototype = this.prototype; // 空对象的原型指向绑定函数的原型
fBound.prototype = new fNOP(); // 空对象的实例赋值给 fBound.prototype
这边可以直接使用ES5的 Object.create()
方法生成一个新对象
fBound.prototype = Object.create(this.prototype);
不过 bind
和 Object.create()
都是ES5方法,部分IE浏览器(IE < 9)并不支持,Polyfill中不能用 Object.create()
实现 bind
,不过原理是一样的。
第四版目前OK啦,代码如下:
// 第四版,已通过测试用例
Function.prototype.bind2 = function (context) { var self = this;
var args = Array.prototype.slice.call(arguments, ); var fNOP = function () {}; var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(
this instanceof fNOP ? this : context,
args.concat(bindArgs)
);
} fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
模拟实现第五步
到这里其实已经差不多了,但有一个问题是调用 bind
的不是函数,这时候需要抛出异常。
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
所以完整版模拟实现代码如下:
// 第五版
Function.prototype.bind2 = function (context) { if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
} var self = this;
var args = Array.prototype.slice.call(arguments, ); var fNOP = function () {}; var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
} fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
【进阶3-4期】深度解析bind原理、使用场景及模拟实现(转)的更多相关文章
- 【进阶3-5期】深度解析 new 原理及模拟实现(转)
这是我在公众号(高级前端进阶)看到的文章,现在做笔记 https://github.com/yygmind/blog/issues/24 new 运算符创建一个用户定义的对象类型的实例或具有构造函数的 ...
- 深度解析 Vue 响应式原理
深度解析 Vue 响应式原理 该文章内容节选自团队的开源项目 InterviewMap.项目目前内容包含了 JS.网络.浏览器相关.性能优化.安全.框架.Git.数据结构.算法等内容,无论是基础还是进 ...
- mysql索引原理深度解析
mysql索引原理深度解析 一.总结 一句话总结: mysql索引是b+树,因为b+树在范围查找.节点查找等方面优化 hash索引,完全平衡二叉树,b树等 1.数据库中最常见的慢查询优化方式是什么? ...
- java8Stream原理深度解析
Java8 Stream原理深度解析 Author:Dorae Date:2017年11月2日19:10:39 转载请注明出处 上一篇文章中简要介绍了Java8的函数式编程,而在Java8中另外一个比 ...
- 并发编程(十五)——定时器 ScheduledThreadPoolExecutor 实现原理与源码深度解析
在上一篇线程池的文章<并发编程(十一)—— Java 线程池 实现原理与源码深度解析(一)>中从ThreadPoolExecutor源码分析了其运行机制.限于篇幅,留下了Scheduled ...
- 并发编程(十二)—— Java 线程池 实现原理与源码深度解析 之 submit 方法 (二)
在上一篇<并发编程(十一)—— Java 线程池 实现原理与源码深度解析(一)>中提到了线程池ThreadPoolExecutor的原理以及它的execute方法.这篇文章是接着上一篇文章 ...
- spring5 源码深度解析----- 被面试官给虐懵了,竟然是因为我不懂@Configuration配置类及@Bean的原理
@Configuration注解提供了全新的bean创建方式.最初spring通过xml配置文件初始化bean并完成依赖注入工作.从spring3.0开始,在spring framework模块中提供 ...
- VueRouter 源码深度解析
VueRouter 源码深度解析 该文章内容节选自团队的开源项目 InterviewMap.项目目前内容包含了 JS.网络.浏览器相关.性能优化.安全.框架.Git.数据结构.算法等内容,无论是基础还 ...
- mybatis 3.x源码深度解析与最佳实践(最完整原创)
mybatis 3.x源码深度解析与最佳实践 1 环境准备 1.1 mybatis介绍以及框架源码的学习目标 1.2 本系列源码解析的方式 1.3 环境搭建 1.4 从Hello World开始 2 ...
随机推荐
- Win10下Prolific USB-to-Serial Comm Port驱动提示不能使用
选择从计算机的设备驱动程序列表中选取 选择第一个安装即可.
- GCC编译器原理(一)------交叉编译器制作和GCC组件及命令
1.1 交叉编译器制作 默认安装的 GCC 编译系统所产生的代码适用于本机,即运行 GCC 的机器,但也可将 GCC 安装成能够生成其他的机器代码.安装一些必须的模块,就可产生多种目标机器代码,而且可 ...
- IEEE LaTeX模板使用BibTeX
IEEE LaTeX 模板使用 BibTeX 在Google Scholar获得的文献引用格式一般是BibTex的,而IEEE Transactions的模板默认用的是BibItem.目前没有什么自动 ...
- luogu 2371 墨墨的等式
1.背包dp #include<bits/stdc++.h> #define rep(i,x,y) for(register int i=x;i<=y;i++) #define ll ...
- Expression 生成 Lambda
public static event Func<Student, bool> myevent; public delegate void del(int i, int j); stati ...
- PHP入门知识
一.搭建开发环境 想要使用一门后端语言,当然是要先搭建开发环境,模拟出服务器环境,不然怎么体现出后端,所以就先大众使用使用的Apache.Mysql,如果不想那么多折腾,建议直接使用xampp或者wa ...
- Python之List列表的循环和切片
一.循环(for):输出列表中的每一个元素 stus=['杨静','王志华','王银梅','乔美玲'] #一个个输出列表元素 for s in stus: print('s 是 %s'%s) s 是 ...
- Java中常见的锁分类以及对应特点
对于 Java 锁的分类没有严格意义的规则,我们常说的分类一般都是依据锁的特性.锁的设计.锁的状态等进行归纳整理的,所以常见的分类如下: 公平锁和非公平锁:公平锁是多线程按照锁申请的顺序获取锁,非公平 ...
- Leetcode#867. Transpose Matrix(转置矩阵)
题目描述 给定一个矩阵 A, 返回 A 的转置矩阵. 矩阵的转置是指将矩阵的主对角线翻转,交换矩阵的行索引与列索引. 示例 1: 输入:[[1,2,3],[4,5,6],[7,8,9]] 输出:[[1 ...
- day 3 - 1 数据类型
什么是数据类型: int 1,2,3用于计算. bool:True,False,用户判断. str:存储少量数据,进行操作 'fjdsal' '二哥','`13243','fdshklj' '战三,李 ...