题目1 手写Object.create
Object.create(obj): 创建以obj为原型的对象
function create(o) {
function F() {};
F.prototype = o;
return new F();
}
这道题的知识点:原型以及原型链,既然涉及到原型,那我们就得画画图, 假如有如下代码:
function A() {}
const a = new A();
const o = {};
首先每个函数都有一个prototype属性,指向函数原型,而对象的实例的话是__proto__属性,这两个都指向Function.prototype。其次,每个原型对象中都有一个constructor指向构造函数。
图错了==
a.proto === A.prototype
一涉及到原型的题目就想起来js中鸡生蛋蛋生鸡的问题,为什么 Object instanceof Function,同时Function instanceof Object,如果要验证这个就要看它的__proto__属性的指向,上图已经画出来了,所以可以直接按照下面的写:
// 如果要解释就是Object是函数,都是由Function创建的实例,所以最关键的是Object的__proto__指向了函数原型
Object instanceof Function:
Object.__proto__ === Function.prototype
// 这里最关键的也是Function由自身构造出来😄,Function的__proto__也是指向的函数原型
// 而函数原型的__proto__指向对象原型,因为原型对象也是个对象
Function instanceof Object:
Function.__proto__ === Function.prototype
Function.prototype.__proto__ === Object.prototype
上面这里还涉及到2个面试中经常出现的问题,instanceof如何实现以及new 如何实现:
题目二 手写instanceof
instanceof用于判断后面的构造函数是否出现在对象的原型中。
function myInstanceof(left, right) {
let proto = Object.getPrototypeOf(left);
const rightProto = right.prototype;
while (proto) {
if (proto === rightProto) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false;
}
题目三 手写new
- 创建一个新对象
- 新对象的原型指向构造函数
- 将构造函数this改为新对象,执行获取返回值
- 如果返回值是对象,则将返回值直接返回,否则返回新创建的对象
function myNew() {
// 取出第一个参数,就是构造函数
const constructor = Array.prototype.shift.call(arguments);
if (typeof constructor !== 'function') {
throw new Error('not function');
}
const o = new Object;
// 原型指向构造函数原型
o.__proto__ = constructor.prototype;
// 改变构造函数的this,使它指向新创建的对象,o可以访问到构造函数的属性。apply参数是数组,call是一个一个的
const res = constructor.apply(o, arguments);
return typeof res === 'object' ? res : o;
}
写完这个题,又引出另外一个题,apply,call,bind的实现😄,这里还有个知识点 typeof 类型判断。
题目四 手写call
定义:Function.prototype.call1(context, ...args), context为要更改为的上下文,其余为其他参数
- context如果不存在,赋值为window
- 将函数赋值为context的一个属性fn
- 调用context.fn并且传入参数
- 删除fn属性
- 返回结果
Function.prototype.call2 = function(context) {
if (typeof this !== 'function') {
throw new Error('type error');
}
context = context || window;
const args = [...arguments].slice(1);
context.fn = this;
const res = context.fn(...args);
delete context.fn;
return res;
}
// 如果不使用es6方法
Function.prototype.call2 = function(context) {
if (typeof this !== 'function') {
throw new Error('type error');
}
context = context || window;
let args = [];
// 结果是['arguments[1]', 'arguments[2]']
for (let i = 1; i < arguments.length; i++) {
args.push('arguments[' + i + ']');
}
context.fn = this;
// args自动调用toString转换为'arguments[1],arguments[2]'
const res = eval('context.fn('+ args + ')');
delete context.fn;
return res;
}
题目五 手写apply
和call基本一致,区别就是参数的传递,apply是传递数组,核心还是以下几步:
- context判空处理
- 函数赋值为context的属性
- 调用context.fn,传入
arguments[1], 记录返回值 - 删除属性
Function.prototype.apply2 = function(context) {
if (typeof this !== 'function') {
throw new Error('type error');
}
context = context || window;
context.fn = this;
// 取第二个参数解构传进去
const res = context.fn(...arguments[1]);
delete context.fn;
return res;
}
// 不使用es6版本
Function.prototype.apply2 = function(context) {
context = context || window;
context.fn = this;
let result;
if (arguments.length === 1) {
result = context.fn();
} else {
resilt = eval('context.fn(' + arguments[1] + ')');
}
delete context.fn;
return result;
}
题目六 手写bind
关键点:
- 返回一个新函数
- 参数都要收集起来
- 兼容构造函数场景:就是bind之后的函数又用了new来调用(这种有实际应用场景吗?。。。)这时候要忽略绑定的上下文。
- 如何判断是new调用场景?就是调用时this是否是返回的bind函数的实例,是的话代表是new调用,要用当前this作为上下文,否则普通场景调用则用传入的context作为上下文。
- 然后还要兼容原型,要把调用bind的原始函数的prototype赋值给新构造的函数,如果直接赋值会出现污染,所以用一个新函数中转,构造一个空函数,将原型赋值为空函数原型,再将新构造的函数原型初始化为F的实例。
/**
* 返回一个新的函数
* 函数参数接收bind的函数和调用的函数,bind函数拼接调用函数构成最后参数
* 如果bind后的函数通过new来调用,则忽略bind绑定的context,使用原来构造函数上下文
*/
Function.prototype.bind2 = function(context) {
const outArgs = [...arguments].slice(1);
const func = this;
function fBound() {
const inArgs = [...arguments];
return func.apply(this instanceof F ? this : context, outArgs.concat(inArgs));
}
function F() {}
F.prototype = this.prototype;
// 断开和原来的原型,防止污染
fBound.prototype = new F();
return fBound;
}