转载的
手写 bind
bind 用法不难,一句话解释就是把新的 this 绑定到某个函数 func 上,并返回 func 的一个拷贝。使用方法如下:
let boundFunc = func.bind(thisArg[, arg1[, arg2[, …argN]]])
那怎么实现呢?我认为手写 bind 可以分为三个等级:
初级:只用 ES6 新语法
优点:因为可以使用 const 、… 操作符,代码简洁
缺点:不兼容 IE
中级:使用 ES5 语法
优点:兼容 IE
缺点:参数要用Array.prototype.slice 取,复杂且不支持 new
高级:ES5 + 支持 new
优点:支持 new
缺点:最复杂
初级 bind
这种方式的优点是因为可以使用 const 、… 操作符,代码简洁;缺点是不兼容 IE 等一些古老浏览器
// 初级:ES6 新语法 const/...
function bind_1(asThis, ...args) {
const fn = this; // 这里的 this 就是调用 bind 的函数 func
return function (...args2) {
return fn.apply(asThis, ...args, ...args2);
};
}
中级 bind
优点:兼容 IE
缺点:参数要用Array.prototype.slice 取,复杂且不支持 new
// 中级:兼容 ES5
function bind_2(asThis) {
var slice = Array.prototype.slice;
var args = slice.call(arguments, 1);
var fn = this;
if (typeof fn !== "function") {
throw new Error("cannot bind non_function");
}
return function () {
var args2 = slice.call(arguments, 0);
return fn.apply(asThis, args.concat(args2));
};
}
高级 bind
优点:支持 new
缺点:最复杂
写之前,我们先来看一看我们应该如何判断 new,
new fn(args) 其实等价于:
const temp = {}
temp.__proto__ = fn.prototype
fn.apply(temp, [...args])
return temp
核心在第二句:temp.proto = fn.prototype,有了这个,我们便知道可以用 fn.prototype 是否为对象原型来判断是否为 new 的情况。
// 高级:支持 new,例如 new (funcA.bind(thisArg, args))
function bind_3(asThis) {
var slice = Array.prototype.slice;
var args1 = slice.call(arguments, 1);
var fn = this;
if (typeof fn !== "function") {
throw new Error("Must accept function");
}
function resultFn() {
var args2 = slice.call(arguments, 0);
return fn.apply(
resultFn.prototype.isPrototypeOf(this) ? this : asThis, // 用来绑定 this
args1.concat(args2)
);
}
resultFn.prototype = fn.prototype;
return resultFn;
}
手写 promise
无疑是要求最高的,如果要硬按照 Promises/A+ 规范来写,可能至少要 2-3 个小时,400+行代码,这种情况是几乎不可能出现在面试中。所以我们只需要完成一个差不多的版本,保留最核心的功能。
核心功能:
new Promise(fn) 其中 fn 只能为函数,且要立即执行
promise.then(success)中的 success 会在 resolve 被调用的时候执行
实现思路:
then(succeed, fail) 先把成功失败回调放到对象实例 callbacks[] 上
resolve() 和 reject() 遍历callbacks
resolve() 读取成功回调 / reject() 读取失败回调(异步等待 then 把回调放好)
执行回调
下面分享我自己根据上述需求及思路实现的模板:
class Promise2 {
state = "pending";
callbacks = [];
constructor(fn) {
if (typeof fn !== "function") {
throw new Error("must pass function");
}
fn(this.resolve.bind(this), this.reject.bind(this));
}
resolve(result) {
if (this.state !== "pending") return;
this.state = "fulfilled";
nextTick(() => {
this.callbacks.forEach((handle) => {
if (typeof handle[0] === "function") {
handle[0].call(undefined, result);
}
});
});
}
reject(reason) {
if (this.state !== "pending") return;
this.state = "rejected";
nextTick(() => {
this.callbacks.forEach((handle) => {
if (typeof handle[1] === "function") {
handle[1].call(undefined, reason);
}
});
});
}
then(succeed, fail) {
const handle = [];
if (typeof succeed === "function") {
handle[0] = succeed;
}
if (typeof fail === "function") {
handle[1] = fail;
}
this.callbacks.push(handle);
}
}
function nextTick(fn) {
if (process !== undefined && typeof process.nextTick === "function") {
return process.nextTick(fn);
} else {
// 实现浏览器上的nextTick
var counter = 1;
var observer = new MutationObserver(fn);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true,
});
counter += 1;
textNode.data = String(counter);
}
}
手写深拷贝
先问这么几个问题,
首先为什么要深拷贝?不希望数据被修改或者只需要部分修改数据。
怎么实现深拷贝?简单需求用 JSON 反序列化,复杂需求用递归克隆。
手写深拷贝的优点?体现扎实的 JS 基础
至于缺点以及如何解决缺点稍后再回答
简单需求
最简单的手写深拷贝就一行,通过 JSON 反序列化来实现
const B = JSON.parse(JSON.stringify(A))
缺点也是显而易见的,JSON value不支持的数据类型,都拷贝不了
不支持函数
不支持undefined(支持null)
不支持循环引用,比如 a = {name: ‘a’}; a.self = a; a2 = JSON.parse(JSON.stringify(a))
不支持Date,会变成 ISO8601 格式的字符串
不支持正则表达式
不支持Symbol
如何支持这些复杂需求,就需要用到递归克隆了。
复杂需求
核心有三点:
递归
对象分类型讨论
解决循环引用(环)
下面给出我的模板
class DeepClone {
constructor() {
this.cacheList = [];
}
clone(source) {
if (source instanceof Object) {
const cache = this.findCache(source);
if (cache) return cache; // 如果找到缓存,直接返回
else {
let target;
if (source instanceof Array) {
target = new Array();
} else if (source instanceof Function) {
target = function () {
return source.apply(this, arguments);
};
} else if (source instanceof Date) {
target = new Date(source);
} else if (source instanceof RegExp) {
target = new RegExp(source.source, source.flags);
}
this.cacheList.push([source, target]); // 把源对象和新对象放进缓存列表
for (let key in source) {
if (source.hasOwnProperty(key)) { // 不拷贝原型上的属性,太浪费内存
target[key] = this.clone(source[key]); // 递归克隆
}
}
return target;
}
}
return source;
}
findCache(source) {
for (let i = 0; i < this.cacheList.length; ++i) {
if (this.cacheList[i][0] === source) {
return this.cacheList[i][1]; // 如果有环,返回对应的新对象
}
}
return undefined;
}
}
这个方案是完美无缺的吗?其实还有不小的距离:
对象类型支持不够多(Buffer,Map,Set等都不支持)
存在递归爆栈的风险
怎么解决?用别人封装好的第三方库。。。
手写 EventHub(发布-订阅)
核心思路是:
使用一个对象作为缓存
on 负责把方法注册到缓存的 EventName 对应的数组
emit 负责遍历触发 EventName 底下的方法数组
off 找方法的索引,并删除
class EventHub {
cache = {};
on(eventName, fn) {
this.cache[eventName] = this.cache[eventName] || [];
this.cache[eventName].push(fn);
}
emit(eventName) {
this.cache[eventName].forEach((fn) => fn());
}
off(eventName, fn) {
const index = indexOf(this.cache[eventName], fn);
if (index === -1) return;
this.cache[eventName].splice(index, 1);
}
}
function indexOf(arr, item) {
if (arr === undefined) return -1;
let index = -1;
for (let i = 0; i < arr.length; ++i) {
if (arr[i] === item) {
index = i;
break;
}
}
return index;
}