函数原型链中的 apply,call 和 bind 方法是 JavaScript 中至关重要的概念,与 this 关键字密切相关,至关一部分人对它们的理解仍是比较浅显,所谓js基础扎实,绕不开这些基础经常使用的API,此次让咱们来完全掌握它们吧!前端
fun.call(thisArg, param1, param2, ...) fun.apply(thisArg, [param1,param2,...]) fun.bind(thisArg, param1, param2, ...)
call/apply:fun
执行的结果
bind:返回fun
的拷贝,并拥有指定的this
值和初始参数react
thisArg
(可选):git
fun
的this
指向thisArg
对象fun
的this
为undefined
param1,param2
(可选): 传给fun
的参数。github
fun
的参数。call
/apply
/bind
的必须是个函数call、apply和bind是挂在Function对象上的三个方法,只有函数才有这些方法。web
只要是函数就能够,好比: Object.prototype.toString
就是个函数,咱们常常看到这样的用法:Object.prototype.toString.call(data)
面试
改变函数执行时的this指向,目前全部关于它们的运用,都是基于这一点来进行的。segmentfault
弄混这两个API的不在少数,不要小看这个问题,记住下面的这个方法就行了。
apply
是以a
开头,它传给fun
的参数是Array
,也是以a
开头的。数组
传给fun
的参数写法不一样:浏览器
apply
是第2个参数,这个参数是一个数组:传给fun
参数都写在数组中。call
从第2~n的参数都是传给fun
的。执行:缓存
返回值:
fun
的执行结果返回值这段在下方bind应用中有详细的示例解析。
看到一个很是棒的例子:
生活中:
平时没时间作饭的我,周末想给孩子炖个腌笃鲜尝尝。可是没有适合的锅,而我又不想出去买。因此就问邻居借了一个锅来用,这样既达到了目的,又节省了开支,一箭双雕。
程序中:
A对象有个方法,B对象由于某种缘由也须要用到一样的方法,那么这时候咱们是单独为 B 对象扩展一个方法呢,仍是借用一下 A 对象的方法呢?
固然是借用 A 对象的方法啦,既达到了目的,又节省了内存。
这就是call/apply/bind的核心理念:借用方法。
借助已实现的方法,改变方法中数据的this指向,减小重复代码,节省内存。
这些应用场景,多加体会就能够发现它们的理念都是:借用方法
Object.prototype.toString
用来判断类型再合适不过,借用它咱们几乎能够判断全部类型的数据:
function isType(data, type) { const typeObj = { '[object String]': 'string', '[object Number]': 'number', '[object Boolean]': 'boolean', '[object Null]': 'null', '[object Undefined]': 'undefined', '[object Object]': 'object', '[object Array]': 'array', '[object Function]': 'function', '[object Date]': 'date', // Object.prototype.toString.call(new Date()) '[object RegExp]': 'regExp', '[object Map]': 'map', '[object Set]': 'set', '[object HTMLDivElement]': 'dom', // document.querySelector('#app') '[object WeakMap]': 'weakMap', '[object Window]': 'window', // Object.prototype.toString.call(window) '[object Error]': 'error', // new Error('1') '[object Arguments]': 'arguments', } let name = Object.prototype.toString.call(data) // 借用Object.prototype.toString()获取数据类型 let typeName = typeObj[name] || '未知类型' // 匹配数据类型 return typeName === type // 判断该数据类型是否为传入的类型 } console.log( isType({}, 'object'), // true isType([], 'array'), // true isType(new Date(), 'object'), // false isType(new Date(), 'date'), // true )
类数组由于不是真正的数组全部没有数组类型上自带的种种方法,因此咱们须要去借用数组的方法。
好比借用数组的push方法:
var arrayLike = { 0: 'OB', 1: 'Koro1', length: 2 } Array.prototype.push.call(arrayLike, '添加元素1', '添加元素2'); console.log(arrayLike) // {"0":"OB","1":"Koro1","2":"添加元素1","3":"添加元素2","length":4}
apply直接传递数组作要调用方法的参数,也省一步展开数组,好比使用Math.max
、Math.min
来获取数组的最大值/最小值:
const arr = [15, 6, 12, 13, 16]; const max = Math.max.apply(Math, arr); // 16 const min = Math.min.apply(Math, arr); // 6
ES5的继承也都是经过借用父类的构造方法来实现父类方法/属性的继承:
// 父类 function supFather(name) { this.name = name; this.colors = ['red', 'blue', 'green']; // 复杂类型 } supFather.prototype.sayName = function (age) { console.log(this.name, 'age'); }; // 子类 function sub(name, age) { // 借用父类的方法:修改它的this指向,赋值父类的构造函数里面方法、属性到子类上 supFather.call(this, name); this.age = age; } // 重写子类的prototype,修正constructor指向 function inheritPrototype(sonFn, fatherFn) { sonFn.prototype = Object.create(fatherFn.prototype); // 继承父类的属性以及方法 sonFn.prototype.constructor = sonFn; // 修正constructor指向到继承的那个函数上 } inheritPrototype(sub, supFather); sub.prototype.sayAge = function () { console.log(this.age, 'foo'); }; // 实例化子类,能够在实例上找到属性、方法 const instance1 = new sub("OBKoro1", 24); const instance2 = new sub("小明", 18); instance1.colors.push('black') console.log(instance1) // {"name":"OBKoro1","colors":["red","blue","green","black"],"age":24} console.log(instance2) // {"name":"小明","colors":["red","blue","green"],"age":18}
相似的应用场景还有不少,就不赘述了,关键在于它们借用方法的理念,不理解的话多看几遍。
call,apply的效果彻底同样,它们的区别也在于
参数数量/顺序不肯定的话就用apply,好比如下示例:
const obj = { age: 24, name: 'OBKoro1', } const obj2 = { age: 777 } callObj(obj, handle) callObj(obj2, handle) // 根据某些条件来决定要传递参数的数量、以及顺序 function callObj(thisAge, fn) { let params = [] if (thisAge.name) { params.push(thisAge.name) } if (thisAge.age) { params.push(thisAge.age) } fn.apply(thisAge, params) // 数量和顺序不肯定 不能使用call } function handle(...params) { console.log('params', params) // do some thing }
首先来看下一道经典的面试题:
for (var i = 1; i <= 5; i++) { setTimeout(function test() { console.log(i) // 依次输出:6 6 6 6 6 }, i * 1000); }
形成这个现象的缘由是等到setTimeout
异步执行时,i
已经变成6了。
关于js事件循环机制不理解的同窗,能够看我这篇博客:Js 的事件循环(Event Loop)机制以及实例讲解
那么如何使他输出: 1,2,3,4,5呢?
方法有不少:
for (var i = 1; i <= 5; i++) { (function (i) { setTimeout(function () { console.log('闭包:', i); // 依次输出:1 2 3 4 5 }, i * 1000); }(i)); }
在这里建立了一个闭包,每次循环都会把i
的最新值传进去,而后被闭包保存起来。
for (var i = 1; i <= 5; i++) { // 缓存参数 setTimeout(function (i) { console.log('bind', i) // 依次输出:1 2 3 4 5 }.bind(null, i), i * 1000); }
实际上这里也用了闭包,咱们知道bind会返回一个函数,这个函数也是闭包。
它保存了函数的this指向、初始参数,每次i
的变动都会被bind的闭包存起来,因此输出1-5。
具体细节,下面有个手写bind方法,研究一下,就能搞懂了。
let
用let
声明i
也能够输出1-5: 由于let
是块级做用域,因此每次都会建立一个新的变量,因此setTimeout
每次读的值都是不一样的,详解。
这是一个常见的问题,下面是我在开发VSCode插件处理webview
通讯时,遇到的真实问题,一开始觉得VSCode的API哪里出问题,调试了一番才发现是this
指向丢失的问题。
class Page { constructor(callBack) { this.className = 'Page' this.MessageCallBack = callBack // this.MessageCallBack('发给注册页面的信息') // 执行PageA的回调函数 } } class PageA { constructor() { this.className = 'PageA' this.pageClass = new Page(this.handleMessage) // 注册页面 传递回调函数 问题在这里 } // 与页面通讯回调 handleMessage(msg) { console.log('处理通讯', this.className, msg) // 'Page' this指向错误 } } new PageA()
this
为什么会丢失?显然声明的时候不会出现问题,执行回调函数的时候也不可能出现问题。
问题出在传递回调函数的时候:
this.pageClass = new Page(this.handleMessage)
由于传递过去的this.handleMessage
是一个函数内存地址,没有上下文对象,也就是说该函数没有绑定它的this
指向。
那它的this
指向于它所应用的绑定规则:
class Page { constructor(callBack) { this.className = 'Page' // callBack() // 直接执行的话 因为class 内部是严格模式,因此this 实际指向的是 undefined this.MessageCallBack = callBack // 回调函数的this 隐式绑定到class page this.MessageCallBack('发给注册页面的信息') } }
既然知道问题了,那咱们只要绑定回调函数的this
指向为PageA
就解决问题了。
回调函数this丢失的解决方案:
bind
绑定回调函数的this
指向:这是典型bind的应用场景, 绑定this指向,用作回调函数。
this.pageClass = new Page(this.handleMessage.bind(this)) // 绑定回调函数的this指向
PS: 这也是为何react
的render
函数在绑定回调函数的时候,也要使用bind绑定一下this
的指向,也是由于一样的问题以及原理。
箭头函数的this指向定义的时候外层第一个普通函数的this,在这里指的是class类:PageA
这块内容,能够看下我以前写的博客:详解箭头函数和普通函数的区别以及箭头函数的注意事项、不适用场景
this.pageClass = new Page(() => this.handleMessage()) // 箭头函数绑定this指向
在大厂的面试中,手写实现call,apply,bind(特别是bind)一直是比较高频的面试题,在这里咱们也一块儿来实现一下这几个函数。
call
吗?思路
this
的指向。context
的属性,将函数的this指向隐式绑定到context上Function.prototype.myCall = function (context, ...arr) { if (context === null || context === undefined) { // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window) context = window } else { context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象 } const specialPrototype = Symbol('特殊属性Symbol') // 用于临时储存函数 context[specialPrototype] = this; // 函数的this指向隐式绑定到context上 let result = context[specialPrototype](...arr); // 经过隐式绑定执行函数并传递参数 delete context[specialPrototype]; // 删除上下文对象的属性 return result; // 返回函数执行结果 };
不少人判断函数上下文对象,只是简单的以context
是否为false来判断,好比:
// 判断函数上下文绑定到`window`不够严谨 context = context ? Object(context) : window; context = context || window;
通过测试,如下三种为false的状况,函数的上下文对象都会绑定到window
上:
// 网上的其余绑定函数上下文对象的方案: context = context || window; function handle(...params) { this.test = 'handle' console.log('params', this, ...params) // do some thing } handle.elseCall('') // window handle.elseCall(0) // window handle.elseCall(false) // window
而call
则将函数的上下文对象会绑定到这些原始值的实例对象上:
因此正确的解决方案,应该是像我上面那么作:
// 正确判断函数上下文对象 if (context === null || context === undefined) { // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window) context = window } else { context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象 }
Symbol
临时储存函数尽管以前用的属性是testFn
但不得不认可,仍是有跟上下文对象的原属性冲突的风险,经网友提醒使用Symbol
就不会出现冲突了。
考虑兼容的话,仍是用尽可能特殊的属性,好比带上本身的ID:OBKoro1TestFn
。
apply
吗?思路:
call
同样。apply
接受第二个参数为类数组对象, 这里用了JavaScript权威指南中判断是否为类数组对象的方法。Function.prototype.myApply = function (context) { if (context === null || context === undefined) { context = window // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window) } else { context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象 } // JavaScript权威指南判断是否为类数组对象 function isArrayLike(o) { if (o && // o不是null、undefined等 typeof o === 'object' && // o是对象 isFinite(o.length) && // o.length是有限数值 o.length >= 0 && // o.length为非负值 o.length === Math.floor(o.length) && // o.length是整数 o.length < 4294967296) // o.length < 2^32 return true else return false } const specialPrototype = Symbol('特殊属性Symbol') // 用于临时储存函数 context[specialPrototype] = this; // 隐式绑定this指向到context上 let args = arguments[1]; // 获取参数数组 let result // 处理传进来的第二个参数 if (args) { // 是否传递第二个参数 if (!Array.isArray(args) && !isArrayLike(args)) { throw new TypeError('myApply 第二个参数不为数组而且不为类数组对象抛出错误'); } else { args = Array.from(args) // 转为数组 result = context[specialPrototype](...args); // 执行函数并展开数组,传递函数参数 } } else { result = context[specialPrototype](); // 执行函数 } delete context[specialPrototype]; // 删除上下文对象的属性 return result; // 返回函数执行结果 };
bind
吗?划重点:
手写bind
是大厂中的一个高频的面试题,若是面试的中高级前端,只是能说出它们的区别,用法并不能脱颖而出,理解要有足够的深度才能抱得offer归!
思路
拷贝源函数:
Object.create
复制源函数的prototype给fToBind调用拷贝的函数:
instanceof
判断函数是否经过new
调用,来决定绑定的context
prototype
的状况Function.prototype.myBind = function (objThis, ...params) { const thisFn = this; // 存储源函数以及上方的params(函数参数) // 对返回的函数 secondParams 二次传参 let fToBind = function (...secondParams) { const isNew = this instanceof fToBind // this是不是fToBind的实例 也就是返回的fToBind是否经过new调用 const context = isNew ? this : Object(objThis) // new调用就绑定到this上,不然就绑定到传入的objThis上 return thisFn.call(context, ...params, ...secondParams); // 用call调用源函数绑定this的指向并传递参数,返回执行结果 }; if (thisFn.prototype) { // 复制源函数的prototype给fToBind 一些状况下函数没有prototype,好比箭头函数 fToBind.prototype = Object.create(thisFn.prototype); } return fToBind; // 返回拷贝的函数 };
prototype
箭头函数没有prototype
,这个我知道的,但是getInfo2
就是一个缩写,为何没有prototype
。
谷歌/stack overflow
都没有找到缘由,有大佬指点迷津一下吗??
var student = { getInfo: function (name, isRegistered) { console.log('this1', this) }, getInfo2(name, isRegistered) { console.log('this2', this) // 没有prototype }, getInfo3: (name, isRegistered) => { console.log('this3', this) // 没有prototype } }
原本觉得这篇会写的很快,结果断断续续的写了好几天,终于把这三个API相关知识介绍清楚了,但愿你们看完以后,面试的时候再遇到这个问题,就能够海陆空全方位的装逼了^_^