bind()

bind() 方法会创建一个新函数,当这个新函数被调用时,它的 this 值是传递给 bind() 的第一个参数,传入bind方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。bind返回的绑定函数也能使用 new 操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。(来自参考1)

语法:fun.bind(thisArg[, arg1[, arg2[, ...]]])

bind 方法与 call / apply 最大的不同就是前者返回一个绑定上下文的函数,而后两者是直接执行了函数。

来个例子说明下

var value = 2;var foo = {value: 1
};function bar(name, age) {return {value: this.value,name: name,age: age}
};bar.call(foo, "Jack", 20); // 直接执行了函数
// {value: 1, name: "Jack", age: 20}var bindFoo1 = bar.bind(foo, "Jack", 20); // 返回一个函数
bindFoo1();
// {value: 1, name: "Jack", age: 20}var bindFoo2 = bar.bind(foo, "Jack"); // 返回一个函数
bindFoo2(20);
// {value: 1, name: "Jack", age: 20}

通过上述代码可以看出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);}, 500);}
}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; // addedsetTimeout(function(){console.log("Hello, my name is " + self.nickname); // changed}, 500);}
}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), 500);}
}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([1, 2, 3]);
// true// 直接使用 toString()
[1, 2, 3].toString();     // "1,2,3"
"123".toString();         // "123"
123.toString();         // SyntaxError: Invalid or unexpected token
Number(123).toString(); // "123"
Object(123).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([1, 2, 3]);
// true// 使用改造后的 toStr
toStr([1, 2, 3]);     // "[object Array]"
toStr("123");         // "[object String]"
toStr(123);         // "[object Number]"
toStr(Object(123)); // "[object Number]"

上面方法首先使用 Function.prototype.call函数指定一个 this 值,然后 .bind 返回一个新的函数,始终将 Object.prototype.toString 设置为传入参数。其实等价于 Object.prototype.toString.call()

这里有一个前提toString()方法没有被覆盖

Object.prototype.toString = function() {return '';
}
isArray([1, 2, 3]);
// false
3、柯里化(curry)

只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

可以一次性地调用柯里化函数,也可以每次只传一个参数分多次调用。

var add = function(x) {return function(y) {return x + y;};
};var increment = add(1);
var addTen = add(10);increment(2);
// 3addTen(2);
// 12add(1)(2);
// 3

这里定义了一个 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 = 2;
var foo = {value: 1
};function bar() {return this.value;
}var bindFoo = bar.bind2(foo);bindFoo(); // 1
模拟实现第二步

对于第 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, 1); return function () {// 实现第4点,这时的arguments是指bind返回的函数传入的参数// 即 return function 的参数var bindArgs = Array.prototype.slice.call(arguments);return self.apply( context, args.concat(bindArgs) );}
}

测试一下:

// 测试用例
var value = 2;var foo = {value: 1
};function bar(name, age) {return {value: this.value,name: name,age: age}
};var bindFoo = bar.bind2(foo, "Jack");
bindFoo(20);
// {value: 1, name: "Jack", age: 20}
模拟实现第三步

到现在已经完成大部分了,但是还有一个难点,bind 有以下一个特性

一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

来个例子说明下:

var value = 2;
var foo = {value: 1
};
function bar(name, age) {this.habit = 'shopping';console.log(this.value);console.log(name);console.log(age);
}
bar.prototype.friend = 'kevin';var bindFoo = bar.bind(foo, 'Jack');
var obj = new bindFoo(20);
// undefined
// Jack
// 20obj.habit;
// shoppingobj.friend;
// kevin

上面例子中,运行结果this.value 输出为 undefined,这不是全局value 也不是foo对象中的value,这说明 bindthis 对象失效了,new 的实现中生成一个新的对象,这个时候的 this指向的是 obj。(【进阶3-1期】有介绍new的实现原理,下一期也会重点介绍)

这里可以通过修改返回函数的原型来实现,代码如下:

// 第三版
Function.prototype.bind2 = function (context) {var self = this;var args = Array.prototype.slice.call(arguments, 1);var fBound = function () {var bindArgs = Array.prototype.slice.call(arguments);// 注释1return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));}// 注释2fBound.prototype = this.prototype;return fBound;
}
  • 注释1:

    • 当作为构造函数时,this 指向实例,此时 this instanceof fBound 结果为 true,可以让实例获得来自绑定函数的值,即上例中实例会具有 habit 属性。
    • 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
  • 注释2: 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值,即上例中 obj 可以获取到 bar 原型上的 friend

注意:这边涉及到了原型、原型链和继承的知识点,可以看下我之前的文章。

JavaScript常用八种继承方案

模拟实现第四步

上面实现中 fBound.prototype = this.prototype有一个缺点,直接修改 fBound.prototype 的时候,也会直接修改 this.prototype

来个代码测试下:

// 测试用例
var value = 2;
var foo = {value: 1
};
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(20); // 返回正确
// undefined
// Jack
// 20obj.habit; // 返回正确
// shoppingobj.friend; // 返回正确
// kevinobj.__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);

不过 bindObject.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, 1);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, 1);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-2期】思考题解

// 1、赋值语句是右执行的,此时会先执行右侧的对象
var obj = {// 2、say 是立即执行函数say: function() {function _say() {// 5、输出 windowconsole.log(this);}// 3、编译阶段 obj 赋值为 undefinedconsole.log(obj);// 4、obj是 undefined,bind 本身是 call实现,// 【进阶3-3期】:call 接收 undefined 会绑定到 window。return _say.bind(obj);}(),
};
obj.say();

【进阶3-3期】思考题解

call 的模拟实现如下,那有没有什么问题呢?

Function.prototype.call = function (context) {context = context || window;context.fn = this;var args = [];for(var i = 1, len = arguments.length; i < len; i++) {args.push('arguments[' + i + ']');}var result = eval('context.fn(' + args +')');delete context.fn;return result;
}

当然是有问题的,其实这里假设 context 对象本身没有 fn 属性,这样肯定不行,我们必须保证 fn属性的唯一性。

ES3下模拟实现

解决方法也很简单,首先判断 context中是否存在属性 fn,如果存在那就随机生成一个属性fnxx,然后循环查询 context 对象中是否存在属性 fnxx。如果不存在则返回最终值。

一种循环方案实现代码如下:

function fnFactory(context) {var unique_fn = "fn";while (context.hasOwnProperty(unique_fn)) {unique_fn = "fn" + Math.random(); // 循环判断并重新赋值}return unique_fn;
}

一种递归方案实现代码如下:

function fnFactory(context) {var unique_fn = "fn" + Math.random();if(context.hasOwnProperty(unique_fn)) {// return arguments.callee(context); ES5 开始禁止使用return fnFactory(context); // 必须 return} else {return unique_fn;}
}

模拟实现完整代码如下:

function fnFactory(context) {var unique_fn = "fn";while (context.hasOwnProperty(unique_fn)) {unique_fn = "fn" + Math.random(); // 循环判断并重新赋值}return unique_fn;
}Function.prototype.call = function (context) {context = context || window;var fn = fnFactory(context); // addedcontext[fn] = this; // changedvar args = [];for(var i = 1, len = arguments.length; i < len; i++) {args.push('arguments[' + i + ']');}var result = eval('context[fn](' + args +')'); // changeddelete context[fn]; // changedreturn result;
}// 测试用例在下面
ES6下模拟实现

ES6有一个新的基本类型Symbol,表示独一无二的值,用法如下。

const symbol1 = Symbol();
const symbol2 = Symbol(42);
const symbol3 = Symbol('foo');console.log(typeof symbol1); // "symbol"
console.log(symbol3.toString()); // "Symbol(foo)"
console.log(Symbol('foo') === Symbol('foo')); // false

不能使用 new 命令,因为这是基本类型的值,不然会报错。

new Symbol();
// TypeError: Symbol is not a constructor

模拟实现完整代码如下:

Function.prototype.call = function (context) {context = context || window;var fn = Symbol(); // addedcontext[fn] = this; // changedlet args = [...arguments].slice(1);let result = context[fn](...args); // changeddelete context[fn]; // changedreturn result;
}
// 测试用例在下面

测试用例在这里:

// 测试用例
var value = 2;
var obj = {value: 1,fn: 123
}function bar(name, age) {console.log(this.value);return {value: this.value,name: name,age: age}
}bar.call(null);
// 2console.log(bar.call(obj, 'kevin', 18));
// 1
// {value: 1, name: "kevin", age: 18}console.log(obj);
// {value: 1, fn: 123}
扩展一下

有两种方案可以判断对象中是否存在某个属性。

var obj = {a: 2
};
Object.prototype.b = function() {return "hello b";
}
  • 1、in 操作符

in 操作符会检查属性是否存在对象及其 [[Prototype]] 原型链中。

("a" in obj);     // true
("b" in obj);     // true
  • 2、Object.hasOwnProperty(...)方法

hasOwnProperty(...)只会检查属性是否存在对象中,不会向上检查其原型链。

obj.hasOwnProperty("a");     //true
obj.hasOwnProperty("b");     //false

注意以下几点:

  • 1、看起来 in 操作符可以检查容器内是否有某个值,实际上检查的是某个属性名是否存在。对于数组来说,4 in [2, 4, 6] 结果返回 false,因为 [2, 4, 6] 这个数组中包含的属性名是0,1,2 ,没有4
  • 2、所有普通对象都可以通过 Object.prototype 的委托来访问 hasOwnProperty(...),但是对于一些特殊对象( Object.create(null) 创建)没有连接到 Object.prototype,这种情况必须使用 Object.prototype.hasOwnProperty.call(obj, "a"),显示绑定到 obj 上。又是一个 call 的用法

本期思考题

用 JS 实现一个无限累加的函数 add,示例如下:

add(1); // 1
add(1)(2);  // 3
add(1)(2)(3); // 6
add(1)(2)(3)(4); // 10 // 以此类推

参考

不用 call 和 apply 方法模拟实现 ES5 的 bind 方法

JavaScript 深入之 bind 的模拟实现

MDN 之 Function.prototype.bind()

MDN 之 Symbol

第 4 章: 柯里化(curry)

进阶系列目录

  • 【进阶1期】 调用堆栈
  • 【进阶2期】 作用域闭包
  • 【进阶3期】 this全面解析
  • 【进阶4期】 深浅拷贝原理
  • 【进阶5期】 原型Prototype
  • 【进阶6期】 高阶函数
  • 【进阶7期】 事件机制
  • 【进阶8期】 Event Loop原理
  • 【进阶9期】 Promise原理
  • 【进阶10期】Async/Await原理
  • 【进阶11期】防抖/节流原理
  • 【进阶12期】模块化详解
  • 【进阶13期】ES6重难点
  • 【进阶14期】计算机网络概述
  • 【进阶15期】浏览器渲染原理
  • 【进阶16期】webpack配置
  • 【进阶17期】webpack原理
  • 【进阶18期】前端监控
  • 【进阶19期】跨域和安全
  • 【进阶20期】性能优化
  • 【进阶21期】VirtualDom原理
  • 【进阶22期】Diff算法
  • 【进阶23期】MVVM双向绑定
  • 【进阶24期】Vuex原理
  • 【进阶25期】Redux原理
  • 【进阶26期】路由原理
  • 【进阶27期】VueRouter源码解析
  • 【进阶28期】ReactRouter源码解析

交流

进阶系列文章汇总如下,内有优质前端资料,觉得不错点个star。

https://github.com/yygmind/blog

我是木易杨,网易高级前端工程师,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!

【进阶3-4期】深度解析bind原理、使用场景及模拟实现相关推荐

  1. 【进阶3-5期】深度解析 new 原理及模拟实现

    定义 new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例. --(来自于MDN) 举个栗子 function Car(color) {this.color = color; ...

  2. 第四十一期:深度解析5G核心网建设难点和挑战

    目前核心网处于架构转型和业务转型的关键期.在架构层面,NFV.CU分离.边缘计算等技术的成熟推动核心网络架构转型,控制面进一步集中,转发面进一步下沉. 目前核心网处于架构转型和业务转型的关键期.在架构 ...

  3. 深度解析HTTPS原理

    HTTPS(全称:HyperText Transfer Protocol over Secure Socket Layer),其实 HTTPS 并不是一个新鲜协议,Google 很早就开始启用了,初衷 ...

  4. 深度解析智能运维场景下“港口行业”解决方案

    行业趋势 基于十四五对"新基建"的要求,结合我国港口发展的阶段性特点,目前智慧港口建设可分为三大方向.一方面是以5G通讯.物联网等新一代信息技术与港口服务深度融合,大力发展智能港口 ...

  5. 推荐:微服务架构的深度解析!

    通过采用微服务架构,企业最大的收益是帮助内部IT建设沿着可演进的方向发展.支持灵活扩展.降低运维成本.快速响应业务变化. 这些底层技术能力的提升让业务更加敏捷.成本可控,企业也可以从中获得技术红利和市 ...

  6. 【进阶4-2期】Object.assign 原理及其实现

    引言 上篇文章介绍了赋值.浅拷贝和深拷贝,其中介绍了很多赋值和浅拷贝的相关知识以及两者区别,限于篇幅只介绍了一种常用深拷贝方案. 本篇文章会先介绍浅拷贝 Object.assign 的实现原理,然后带 ...

  7. 《Spring源码深度解析 郝佳 第2版》SpringBoot体系分析、Starter的原理

    往期博客 <Spring源码深度解析 郝佳 第2版>容器的基本实现与XML文件的加载 <Spring源码深度解析 郝佳 第2版>XML标签的解析 <Spring源码深度解 ...

  8. 《Spring源码深度解析 郝佳 第2版》JDBC、MyBatis原理

    往期博客 <Spring源码深度解析 郝佳 第2版>容器的基本实现与XML文件的加载 <Spring源码深度解析 郝佳 第2版>XML标签的解析 <Spring源码深度解 ...

  9. 单文件浏览器_图文并茂深度解析浏览器渲染原理,包看懂超值得收藏

    在我们面试过程中,面试官经常会问到这么一个问题,那就是从在浏览器地址栏中输入URL到页面显示,浏览器到底发生了什么?这个问题看起来是老生常谈,但是这个问题回答的好坏,确实可以很好的反映出面试者知识的广 ...

最新文章

  1. 控件联动(三级联动)
  2. 对于计算机文化的总结,计算机文化基础一级总结
  3. 3亿Docker容器部署的挑战及应对方案
  4. css知识笔记(五)——css样式设置小技巧
  5. 字符串匹配:字符串中查找某子串
  6. erlang rebar 配置mysql_Centos6.4安装erlang并配置mysql数据库
  7. Gitlab+Jenkins学习之路(四)之gitlab备份和恢复
  8. oracle四大索引类型,oracle 索引类型
  9. web漏洞扫描器原理_基于指纹识别的漏洞扫描设计
  10. 快速使用 Javassist
  11. selenium+python+eclipse 实现 “问卷星”网站,登录与检查登录示例!
  12. 利用ESP定律进行脱壳 ——合天网安实验室学习笔记
  13. IAR 开发Zigbee 下载调试时总是出现target selection对话框
  14. calico源码分析-ipam(1)
  15. 原型法和面向对象的分析与设计方法
  16. javascript原型、原型链神图
  17. 用C语言进行公英单位转换方法
  18. win10家庭中文版安装Hyper-V
  19. 服务器里怎么设置微信多开,私域必备,企业微信多开的4种方法
  20. I - Monthly Expense POJ - 3273

热门文章

  1. 交通银行软件中心编制_智能运维国家标准编制启动会在京举行 云智慧参与标准制定...
  2. 商标注册流程与注意事项 logo 商标注册类型分类解释
  3. 【无标题】排序算法(C语言)
  4. 东风小康为什么是dfsk_重庆造乘用车首次乘坐专列出口欧洲 100辆东风风光ix5抵达德国...
  5. python里none什么意思_Python 中None的用法
  6. pg 定时删除_定时删除网站文件
  7. php 统计页面跳失率,究竟网店各页面的跳失率大小为多少才算正常水平?
  8. ipv6单播地址包括哪两种类型_IPv6基础介绍
  9. 关于项目404解决方法:前提条件:项目路径都是正确的
  10. xcode修改时间后就要重新编译_[NewLife.XCode]反向工程(自动建表建库大杀器)