比如借用数组的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获取数组最大值最小值:
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,该用哪个?、call,apply的效果完全一样,它们的区别也在于
参数数量/顺序确定就用call,参数数量/顺序不确定的话就用apply。
考虑可读性:参数数量不多就用apply,参数数量比较多的话,把参数整合成数组,使用apply。
参数集合已经是一个数组的情况,用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 } bind的应用场景: 1. 保存函数参数:首先来看下一道经典的面试题:
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的最新值传进去,然后被闭包保存起来。
bind
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每次读的值都是不同的,详解。
2. 回调函数this丢失问题:这是一个常见的问题,下面是我在开发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指向。