面試官問:能否模擬實現(xiàn)JS的bind方法(高頻考點)
寫于2018年11月21日,發(fā)布在掘金閱讀量1.3w+
前言
這是面試官問系列的第二篇,旨在幫助讀者提升
JS基礎(chǔ)知識,包含new、call、apply、this、繼承相關(guān)知識。面試官問系列文章如下:感興趣的讀者可以點擊閱讀。
1.面試官問:能否模擬實現(xiàn)JS的new操作符
2.面試官問:能否模擬實現(xiàn)JS的bind方法(本文)
3.面試官問:能否模擬實現(xiàn)JS的call和apply方法
4.面試官問:JS的this指向
5.面試官問:JS的繼承
用過React的同學(xué)都知道,經(jīng)常會使用bind來綁定this。
import React, { Component } from 'react';
class TodoItem extends Component{
constructor(props){
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(){
console.log('handleClick');
}
render(){
return (
<div onClick={this.handleClick}>點擊</div>
);
};
}
export default TodoItem;
那么面試官可能會問是否想過bind到底做了什么,怎么模擬實現(xiàn)呢。
附上之前寫文章寫過的一段話:已經(jīng)有很多模擬實現(xiàn)
bind的文章,為什么自己還要寫一遍呢。學(xué)習(xí)就好比是座大山,人們沿著不同的路登山,分享著自己看到的風(fēng)景。你不一定能看到別人看到的風(fēng)景,體會到別人的心情。只有自己去登山,才能看到不一樣的風(fēng)景,體會才更加深刻。
先看一下bind是什么。從上面的React代碼中,可以看出bind執(zhí)行后是函數(shù),并且每個函數(shù)都可以執(zhí)行調(diào)用它。眼見為實,耳聽為虛。讀者可以在控制臺一步步點開例子1中的obj:
var obj = {};
console.log(obj);
console.log(typeof Function.prototype.bind); // function
console.log(typeof Function.prototype.bind()); // function
console.log(Function.prototype.bind.name); // bind
console.log(Function.prototype.bind().name); // bound

Function.prototype.bind因此可以得出結(jié)論1:
1、bind是Functoin原型鏈中Function.prototype的一個屬性,每個函數(shù)都可以調(diào)用它。
2、bind本身是一個函數(shù)名為bind的函數(shù),返回值也是函數(shù),函數(shù)名是bound。(打出來就是bound加上一個空格)。知道了bind是函數(shù),就可以傳參,而且返回值'bound '也是函數(shù),也可以傳參,就很容易寫出例子2:
后文統(tǒng)一 bound 指原函數(shù)original bind之后返回的函數(shù),便于說明。
var obj = {
name: '若川',
};
function original(a, b){
console.log(this.name);
console.log([a, b]);
return false;
}
var bound = original.bind(obj, 1);
var boundResult = bound(2); // '若川', [1, 2]
console.log(boundResult); // false
console.log(original.bind.name); // 'bind'
console.log(original.bind.length); // 1
console.log(original.bind().length); // 2 返回original函數(shù)的形參個數(shù)
console.log(bound.name); // 'bound original'
console.log((function(){}).bind().name); // 'bound '
console.log((function(){}).bind().length); // 0
由此可以得出結(jié)論2:
1、調(diào)用bind的函數(shù)中的this指向bind()函數(shù)的第一個參數(shù)。
2、傳給bind()的其他參數(shù)接收處理了,bind()之后返回的函數(shù)的參數(shù)也接收處理了,也就是說合并處理了。
3、并且bind()后的name為bound + 空格 + 調(diào)用bind的函數(shù)名。如果是匿名函數(shù)則是bound + 空格。
4、bind后的返回值函數(shù),執(zhí)行后返回值是原函數(shù)(original)的返回值。
5、bind函數(shù)形參(即函數(shù)的length)是1。bind后返回的bound函數(shù)形參不定,根據(jù)綁定的函數(shù)原函數(shù)(original)形參個數(shù)確定。
根據(jù)結(jié)論2:我們就可以簡單模擬實現(xiàn)一個簡版bindFn
// 第一版 修改this指向,合并參數(shù)
Function.prototype.bindFn = function bind(thisArg){
if(typeof this !== 'function'){
throw new TypeError(this + 'must be a function');
}
// 存儲函數(shù)本身
var self = this;
// 去除thisArg的其他參數(shù) 轉(zhuǎn)成數(shù)組
var args = [].slice.call(arguments, 1);
var bound = function(){
// bind返回的函數(shù) 的參數(shù)轉(zhuǎn)成數(shù)組
var boundArgs = [].slice.call(arguments);
// apply修改this指向,把兩個函數(shù)的參數(shù)合并傳給self函數(shù),并執(zhí)行self函數(shù),返回執(zhí)行結(jié)果
return self.apply(thisArg, args.concat(boundArgs));
}
return bound;
}
// 測試
var obj = {
name: '若川',
};
function original(a, b){
console.log(this.name);
console.log([a, b]);
}
var bound = original.bindFn(obj, 1);
bound(2); // '若川', [1, 2]
如果面試官看到你答到這里,估計對你的印象60、70分應(yīng)該是會有的。但我們知道函數(shù)是可以用new來實例化的。那么bind()返回值函數(shù)會是什么表現(xiàn)呢。
接下來看例子3:
var obj = {
name: '若川',
};
function original(a, b){
console.log('this', this); // original {}
console.log('typeof this', typeof this); // object
this.name = b;
console.log('name', this.name); // 2
console.log('this', this); // original {name: 2}
console.log([a, b]); // 1, 2
}
var bound = original.bind(obj, 1);
var newBoundResult = new bound(2);
console.log(newBoundResult, 'newBoundResult'); // original {name: 2}
從例子3種可以看出this指向了new bound()生成的新對象。
可以分析得出結(jié)論3:
1、bind原先指向obj的失效了,其他參數(shù)有效。
2、new bound的返回值是以original原函數(shù)構(gòu)造器生成的新對象。original原函數(shù)的this指向的就是這個新對象。另外前不久寫過一篇文章:面試官問:能否模擬實現(xiàn)JS的new操作符。簡單摘要:new做了什么:
1.創(chuàng)建了一個全新的對象。
2.這個對象會被執(zhí)行[[Prototype]](也就是__proto__)鏈接。
3.生成的新對象會綁定到函數(shù)調(diào)用的this。
4.通過new創(chuàng)建的每個對象將最終被[[Prototype]]鏈接到這個函數(shù)的prototype對象上。
5.如果函數(shù)沒有返回對象類型Object(包含Functoin,Array,Date,RegExg,Error),那么new表達式中的函數(shù)調(diào)用會自動返回這個新的對象。
所以相當(dāng)于new調(diào)用時,bind的返回值函數(shù)bound內(nèi)部要模擬實現(xiàn)new實現(xiàn)的操作。話不多說,直接上代碼。
// 第三版 實現(xiàn)new調(diào)用
Function.prototype.bindFn = function bind(thisArg){
if(typeof this !== 'function'){
throw new TypeError(this + ' must be a function');
}
// 存儲調(diào)用bind的函數(shù)本身
var self = this;
// 去除thisArg的其他參數(shù) 轉(zhuǎn)成數(shù)組
var args = [].slice.call(arguments, 1);
var bound = function(){
// bind返回的函數(shù) 的參數(shù)轉(zhuǎn)成數(shù)組
var boundArgs = [].slice.call(arguments);
var finalArgs = args.concat(boundArgs);
// new 調(diào)用時,其實this instanceof bound判斷也不是很準確。es6 new.target就是解決這一問題的。
if(this instanceof bound){
// 這里是實現(xiàn)上文描述的 new 的第 1, 2, 4 步
// 1.創(chuàng)建一個全新的對象
// 2.并且執(zhí)行[[Prototype]]鏈接
// 4.通過`new`創(chuàng)建的每個對象將最終被`[[Prototype]]`鏈接到這個函數(shù)的`prototype`對象上。
// self可能是ES6的箭頭函數(shù),沒有prototype,所以就沒必要再指向做prototype操作。
if(self.prototype){
// ES5 提供的方案 Object.create()
// bound.prototype = Object.create(self.prototype);
// 但 既然是模擬ES5的bind,那瀏覽器也基本沒有實現(xiàn)Object.create()
// 所以采用 MDN ployfill方案 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create
function Empty(){}
Empty.prototype = self.prototype;
bound.prototype = new Empty();
}
// 這里是實現(xiàn)上文描述的 new 的第 3 步
// 3.生成的新對象會綁定到函數(shù)調(diào)用的`this`。
var result = self.apply(this, finalArgs);
// 這里是實現(xiàn)上文描述的 new 的第 5 步
// 5.如果函數(shù)沒有返回對象類型`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`),
// 那么`new`表達式中的函數(shù)調(diào)用會自動返回這個新的對象。
var isObject = typeof result === 'object' && result !== null;
var isFunction = typeof result === 'function';
if(isObject || isFunction){
return result;
}
return this;
}
else{
// apply修改this指向,把兩個函數(shù)的參數(shù)合并傳給self函數(shù),并執(zhí)行self函數(shù),返回執(zhí)行結(jié)果
return self.apply(thisArg, finalArgs);
}
};
return bound;
}
面試官看到這樣的實現(xiàn)代碼,基本就是滿分了,心里獨白:這小伙子/小姑娘不錯啊。不過可能還會問this instanceof bound不準確問題。上文注釋中提到this instanceof bound也不是很準確,ES6 new.target很好的解決這一問題,我們舉個例子4:
instanceof 不準確,ES6 new.target很好的解決這一問題
function Student(name){
if(this instanceof Student){
this.name = name;
console.log('name', name);
}
else{
throw new Error('必須通過new關(guān)鍵字來調(diào)用Student。');
}
}
var student = new Student('若');
var notAStudent = Student.call(student, '川'); // 不拋出錯誤,且執(zhí)行了。
console.log(student, 'student', notAStudent, 'notAStudent');
function Student2(name){
if(typeof new.target !== 'undefined'){
this.name = name;
console.log('name', name);
}
else{
throw new Error('必須通過new關(guān)鍵字來調(diào)用Student2。');
}
}
var student2 = new Student2('若');
var notAStudent2 = Student2.call(student2, '川');
console.log(student2, 'student2', notAStudent2, 'notAStudent2'); // 拋出錯誤
細心的同學(xué)可能會發(fā)現(xiàn)了這版本的代碼沒有實現(xiàn)bind后的bound函數(shù)的nameMDN Function.name和lengthMDN Function.length。面試官可能也發(fā)現(xiàn)了這一點繼續(xù)追問,如何實現(xiàn),或者問是否看過es5-shim的源碼實現(xiàn)L201-L335。如果不限ES版本。其實可以用ES5的Object.defineProperties來實現(xiàn)。
Object.defineProperties(bound, {
'length': {
value: self.length,
},
'name': {
value: 'bound ' + self.name,
}
});
es5-shim的源碼實現(xiàn)bind
直接附上源碼(有刪減注釋和部分修改等)
var $Array = Array;
var ArrayPrototype = $Array.prototype;
var $Object = Object;
var array_push = ArrayPrototype.push;
var array_slice = ArrayPrototype.slice;
var array_join = ArrayPrototype.join;
var array_concat = ArrayPrototype.concat;
var $Function = Function;
var FunctionPrototype = $Function.prototype;
var apply = FunctionPrototype.apply;
var max = Math.max;
// 簡版 源碼更復(fù)雜些。
var isCallable = function isCallable(value){
if(typeof value !== 'function'){
return false;
}
return true;
};
var Empty = function Empty() {};
// 源碼是 defineProperties
// 源碼是bind筆者改成bindFn便于測試
FunctionPrototype.bindFn = function bind(that) {
var target = this;
if (!isCallable(target)) {
throw new TypeError('Function.prototype.bind called on incompatible ' + target);
}
var args = array_slice.call(arguments, 1);
var bound;
var binder = function () {
if (this instanceof bound) {
var result = apply.call(
target,
this,
array_concat.call(args, array_slice.call(arguments))
);
if ($Object(result) === result) {
return result;
}
return this;
} else {
return apply.call(
target,
that,
array_concat.call(args, array_slice.call(arguments))
);
}
};
var boundLength = max(0, target.length - args.length);
var boundArgs = [];
for (var i = 0; i < boundLength; i++) {
array_push.call(boundArgs, '$' + i);
}
// 這里是Function構(gòu)造方式生成形參length $1, $2, $3...
bound = $Function('binder', 'return function (' + array_join.call(boundArgs, ',') + '){ return binder.apply(this, arguments); }')(binder);
if (target.prototype) {
Empty.prototype = target.prototype;
bound.prototype = new Empty();
Empty.prototype = null;
}
return bound;
};
你說出es5-shim源碼bind實現(xiàn),感慨這代碼真是高效、嚴謹。面試官心里獨白可能是:你就是我要找的人,薪酬福利你可以和HR去談下。
最后總結(jié)一下
1、bind是Function原型鏈中的Function.prototype的一個屬性,它是一個函數(shù),修改this指向,合并參數(shù)傳遞給原函數(shù),返回值是一個新的函數(shù)。
2、bind返回的函數(shù)可以通過new調(diào)用,這時提供的this的參數(shù)被忽略,指向了new生成的全新對象。內(nèi)部模擬實現(xiàn)了new操作符。
3、es5-shim源碼模擬實現(xiàn)bind時用Function實現(xiàn)了length。
事實上,平時其實很少需要使用自己實現(xiàn)的投入到生成環(huán)境中。但面試官通過這個面試題能考察很多知識。比如this指向,原型鏈,閉包,函數(shù)等知識,可以擴展很多。
讀者發(fā)現(xiàn)有不妥或可改善之處,歡迎指出。另外覺得寫得不錯,可以點個贊,也是對筆者的一種支持。
文章中的例子和測試代碼放在github中bind模擬實現(xiàn) github。bind模擬實現(xiàn) 預(yù)覽地址 F12看控制臺輸出,結(jié)合source面板查看效果更佳。
// 最終版 刪除注釋 詳細注釋版請看上文
Function.prototype.bind = Function.prototype.bind || function bind(thisArg){
if(typeof this !== 'function'){
throw new TypeError(this + ' must be a function');
}
var self = this;
var args = [].slice.call(arguments, 1);
var bound = function(){
var boundArgs = [].slice.call(arguments);
var finalArgs = args.concat(boundArgs);
if(this instanceof bound){
if(self.prototype){
function Empty(){}
Empty.prototype = self.prototype;
bound.prototype = new Empty();
}
var result = self.apply(this, finalArgs);
var isObject = typeof result === 'object' && result !== null;
var isFunction = typeof result === 'function';
if(isObject || isFunction){
return result;
}
return this;
}
else{
return self.apply(thisArg, finalArgs);
}
};
return bound;
}
推薦閱讀
我在阿里招前端,我該怎么幫你?(現(xiàn)在還可以加模擬面試群)
如何拿下阿里巴巴 P6 的前端 Offer
如何準備阿里P6/P7前端面試--項目經(jīng)歷準備篇
大廠面試官常問的亮點,該如何做出?
如何從初級到專家(P4-P7)打破成長瓶頸和有效突破
若川知乎問答:2年前端經(jīng)驗,做的項目沒什么技術(shù)含量,怎么辦?
若川知乎高贊:有哪些必看的 JS庫?
末尾
你好,我是若川,江湖人稱菜如若川,歷時一年只寫了一個學(xué)習(xí)源碼整體架構(gòu)系列~(點擊藍字了解我)
關(guān)注 若川視野,回復(fù)"pdf" 領(lǐng)取優(yōu)質(zhì)前端書籍pdf,回復(fù)"1",可加群長期交流學(xué)習(xí)我的博客地址:https://lxchuan12.gitee.io 歡迎收藏 覺得文章不錯,可以點個 在看呀^_^另外歡迎留言交流~


