X

头条面试官问我的几种源码实现,还好我都会

阅读源码的好处,不用说都知道,首先进大厂必备,还可以提升自己的能力,学习前人的经验。源码往往是前人留下的最佳实践,我们跟着前人的脚步去学习会让我们事半功倍。

  • call、aplly、bind 实现
  • new 实现
  • class 实现继承
  • async/await 实现
  • reduce 实现
  • 实现一个双向数据绑定
  • instanceof 实现
  • Array.isArray 实现
  • Object.create 的基本实现原理
  • getOwnPropertyNames 实现
  • promise 实现
  • 手写一个防抖/节流函数
  • 柯里化函数的实现
  • 手写一个深拷贝

call、aplly、bind 实现

call、aplly、bind 本质都是改变 this 的指向,不同点 call、aplly 是直接调用函数,bind 是返回一个新的函数。call 跟 aplly 就只有参数上不同。

bind 实现

  • 箭头函数的 this 永远指向它所在的作用域
  • 函数作为构造函数用 new 关键字调用时,不应该改变其 this 指向,因为 new绑定 的优先级高于 显示绑定 和 硬绑定

Function.prototype.mybind = function(thisArg) {
if (typeof this !== ‘function’) {
throw TypeError(“Bind must be called on a function”);
}
// 拿到参数,为了传给调用者
const args = Array.prototype.slice.call(arguments, 1),
// 保存 this
self = this,
// 构建一个干净的函数,用于保存原函数的原型
nop = function() {},
// 绑定的函数
bound = function() {
// this instanceof nop, 判断是否使用 new 来调用 bound
// 如果是 new 来调用的话,this的指向就是其实例,
// 如果不是 new 调用的话,就改变 this 指向到指定的对象 o
return self.apply(
this instanceof nop ? this : thisArg,
args.concat(Array.prototype.slice.call(arguments))
);
};

// 箭头函数没有 prototype,箭头函数this永远指向它所在的作用域
if (this.prototype) {
nop.prototype = this.prototype;
}
// 修改绑定函数的原型指向
bound.prototype = new nop();

return bound;
}
}

  1. 测试 mybind

const bar = function() {
console.log(this.name, arguments);
};

bar.prototype.name = ‘bar’;

const foo = {
name: ‘foo’
};

const bound = bar.mybind(foo, 22, 33, 44);
new bound(); // bar, [22, 33, 44]
bound(); // foo, [22, 33, 44]

call 实现

bind 是封装了 call 的方法改变了 this 的指向并返回一个新的函数,那么 call 是如何做到改变 this 的指向呢?原理很简单,在方法调用模式下,this 总是指向调用它所在方法的对象,this 的指向与所在方法的调用位置有关,而与方法的声明位置无关(箭头函数特殊)。先写一个小 demo 来理解一下下。

const foo = { name: ‘foo’ };

foo.fn = function() {
// 这里的 this 指向了 foo
// 因为 foo 调用了 fn,
// fn 的 this 就指向了调用它所在方法的对象 foo 上
console.log(this.name); // foo
};

利用 this 的机制来实现 call

Function.prototype.mycall = function(thisArg) {
// this指向调用call的对象
if (typeof this !== ‘function’) {
// 调用call的若不是函数则报错
throw new TypeError(‘Error’);
}

const args = […arguments].slice(1);
thisArg = thisArg || window;
// 将调用call函数的对象添加到thisArg的属性中
thisArg.fn = this;
// 执行该属性
const result = thisArg.fn(…arg);
// 删除该属性
delete thisArg.fn;
// 返回函数执行结果
return result;
};

aplly 实现

Function.prototype.myapply = function(thisArg) {
if (typeof this !== ‘function’) {
throw this + ‘ is not a function’;
}

const args = arguments[1];

thisArg.fn = this;

const result = thisArg.fn(…arg);

delete thisArg.fn;

return result;
};

测试 mycall myaplly

const bar = function() {
console.log(this.name, arguments);
};

bar.prototype.name = ‘bar’;

const foo = {
name: ‘foo’
};

bar.mycall(foo, 1, 2, 3); // foo [1, 2, 3]
bar.myaplly(foo, [1, 2, 3]); // foo [1, 2, 3]

reduce 实现原理

arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

Array.prototype.myreduce = function reduce(callbackfn) {
// 拿到数组
const O = this,
len = O.length;
// 下标值
let k = 0,
// 累加器
accumulator = undefined,
// k下标对应的值是否存在
kPresent = false,
// 初始值
initialValue = arguments.length > 1 ? arguments[1] : undefined;

if (typeof callbackfn !== ‘function’) {
throw new TypeError(callbackfn + ‘ is not a function’);
}

// 数组为空,并且有初始值,报错
if (len === 0 && arguments.length < 2) {
throw new TypeError(‘Reduce of empty array with no initial value’);
}

// 如果初始值存在
if (arguments.length > 1) {
// 设置累加器为初始值
accumulator = initialValue;
// 初始值不存在
} else {
accumulator = O[k];
++k;
}

while (k < len) {
// 判断是否为 empty [,,,]
kPresent = O.hasOwnProperty(k);

if (kPresent) {
const kValue = O[k];
// 调用 callbackfn
accumulator = callbackfn.apply(undefined, [accumulator, kValue, k, O]);
}
++k;
}

return accumulator;
};

测试

const rReduce = [‘1’, null, undefined, , 3, 4].reduce((a, b) => a + b, 3);
const mReduce = [‘1’, null, undefined, , 3, 4].myreduce((a, b) => a + b, 3);

console.log(rReduce, mReduce);
// 31nullundefined34 31nullundefined34

new 实现

我们需要知道当 new 的时候做了什么事情

  1. 创建一个新对象;
  2. 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象。

因为 new 没办法重写,我们使用 myNew 函数来模拟 new

function myNew() {
// 创建一个实例对象
var obj = new Object();
// 取得外部传入的构造器
var Constructor = Array.prototype.shift.call(arguments);
// 实现继承,实例可以访问构造器的属性
obj.__proto__ = Constructor.prototype;
// 调用构造器,并改变其 this 指向到实例
var ret = Constructor.apply(obj, arguments);
// 如果构造函数返回值是对象则返回这个对象,如果不是对象则返回新的实例对象
return typeof ret === ‘object’ ? ret : obj;
}

测试 myNew

// ========= 无返回值 =============
const testNewFun = function(name) {
this.name = name;
};

const newObj = myNew(testNewFun, ‘foo’);

console.log(newObj); // { name: “foo” }
console.log(newObj instanceof testNewFun); // true
// ========= 有返回值 =============
const testNewFun = function(name) {
this.name = name;
return {};
};

const newObj = myNew(testNewFun, ‘foo’);

console.log(newObj); // {}
console.log(newObj instanceof testNewFun); // false

class 实现继承

主要使用 es5 跟 es6 对比看下 class 继承的原理

实现继承 A extends B

使用 es6 语法

class B {
constructor(opt) {
this.BName = opt.name;
}
}
class A extends B {
constructor() {
// 向父类传参
super({ name: ‘B’ });
// this 必须在 super() 下面使用
console.log(this);
}
}

使用 es5 语法

使用寄生组合继承的方式

  1. 原型链继承,使子类可以调用父类原型上的方法和属性
  2. 借用构造函数继承,可以实现向父类传参
  3. 寄生继承,创造干净的没有构造方法的函数,用来寄生父类的 prototype

// 实现继承,通过继承父类 prototype
function __extends(child, parent) {
// 修改对象原型
Object.setPrototypeOf(child, parent);
// 寄生继承,创建一个干净的构造函数,用于继承父类的 prototype
// 这样做的好处是,修改子类的 prototype 不会影响父类的 prototype
function __() {
// 修正 constructor 指向子类
this.constructor = child;
}
// 原型继承,继承父类原型属性,但是无法向父类构造函数传参
child.prototype =
parent === null
? Object.create(parent)
: ((__.prototype = parent.prototype), new __());
}

var B = (function() {
function B(opt) {
this.name = opt.name;
}
return B;
})();

var A = (function(_super) {
__extends(A, _super);
function A() {
// 借用继承,可以实现向父类传参, 使用 super 可以向父类传参
return (_super !== null && _super.apply(this, { name: ‘B’ })) || this;
}
return A;
})(B);

测试 class

const a = new A();

console.log(a.BName, a.constructor); // B ,ƒ A() {}

async/await 实现

原理就是利用 generator(生成器)分割代码片段。然后我们使用一个函数让其自迭代,每一个yield 用 promise 包裹起来。执行下一步的时机由 promise 来控制

async/await 是关键字,不能重写它的方法,我们使用函数来模拟

异步迭代,模拟异步函数

function _asyncToGenerator(fn) {
return function() {
var self = this,
args = arguments;
// 将返回值promise化
return new Promise(function(resolve, reject) {
// 获取迭代器实例
var gen = fn.apply(self, args);
// 执行下一步
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, ‘next’, value);
}
// 抛出异常
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, ‘throw’, err);
}
// 第一次触发
_next(undefined);
});
};
}

执行迭代步骤,处理下次迭代结果

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
try {
var info = gen[key](arg);
var value = info.value;
} catch (error) {
reject(error);
return;
}
if (info.done) {
// 迭代器完成
resolve(value);
} else {
// — 这行代码就是精髓 —
// 将所有值promise化
// 比如 yield 1
// const a = Promise.resolve(1) a 是一个 promise
// const b = Promise.resolve(a) b 是一个 promise
// 可以做到统一 promise 输出
// 当 promise 执行完之后再执行下一步
// 递归调用 next 函数,直到 done == true
Promise.resolve(value).then(_next, _throw);
}
}

测试 _asyncToGenerator

const asyncFunc = _asyncToGenerator(function*() {
const e = yield new Promise(resolve => {
setTimeout(() => {
resolve(‘e’);
}, 1000);
});
const a = yield Promise.resolve(‘a’);
const d = yield ‘d’;
const b = yield Promise.resolve(‘b’);
const c = yield Promise.resolve(‘c’);
return [a, b, c, d, e];
});

asyncFunc().then(res => {
console.log(res); // [‘a’, ‘b’, ‘c’, ‘d’, ‘e’]
});

实现一个双向绑定

defineProperty 版本

// 数据
const data = {
text: ‘default’
};
const input = document.getElementById(‘input’);
const span = document.getElementById(‘span’);
// 数据劫持
Object.defineProperty(data, ‘text’, {
// 数据变化 –> 修改视图
set(newVal) {
input.value = newVal;
span.innerHTML = newVal;
}
});
// 视图更改 –> 数据变化
input.addEventListener(‘keyup’, function(e) {
data.text = e.target.value;
});

proxy 版本

// 数据
const data = {
text: ‘default’
};
const input = document.getElementById(‘input’);
const span = document.getElementById(‘span’);
// 数据劫持
const handler = {
set(target, key, value) {
target[key] = value;
// 数据变化 –> 修改视图
input.value = value;
span.innerHTML = value;
return value;
}
};
const proxy = new Proxy(data);

// 视图更改 –> 数据变化
input.addEventListener(‘keyup’, function(e) {
proxy.text = e.target.value;
});

Object.create 的基本实现原理

function create(obj) {
function F() {}
F.prototype = obj;
return new F();
}

instanceof 实现

原理:L 的 __proto__ 是不是等于 R.prototype,不等于再找 L.__proto__.__proto__ 直到 __proto__ 为 null

// L 表示左表达式,R 表示右表达式
function instance_of(L, R) {
var O = R.prototype;
L = L.__proto__;
while (true) {
if (L === null) return false;
// 这里重点:当 O 严格等于 L 时,返回 true
if (O === L) return true;
L = L.__proto__;
}
}

Array.isArray 实现

Array.myIsArray = function(o) {
return Object.prototype.toString.call(Object(o)) === ‘[object Array]’;
};

console.log(Array.myIsArray([])); // true

getOwnPropertyNames 实现

if (typeof Object.getOwnPropertyNames !== ‘function’) {
Object.getOwnPropertyNames = function(o) {
if (o !== Object(o)) {
throw TypeError(‘Object.getOwnPropertyNames called on non-object’);
}
var props = [],
p;
for (p in o) {
if (Object.prototype.hasOwnProperty.call(o, p)) {
props.push(p);
}
}
return props;
};
}

Promise 实现

实现原理:其实就是一个发布订阅者模式

  1. 构造函数接收一个 executor 函数,并会在 new Promise() 时立即执行该函数
  2. then 时收集依赖,将回调函数收集到 成功/失败队列
  3. executor 函数中调用 resolve/reject 函数
  4. resolve/reject 函数被调用时会通知触发队列中的回调

先看一下整体代码,有一个大致的概念