【业精于勤】基础前端面试题整理

1. 实现一个 new

new 一个对象会发生如下的步骤,基于这些步骤我们来尝试想一下怎么实现它们:

  1. 创建或者说事构造一个全新的对象(创建一个空对象);
  2. 这个全新的对象会进行 [[Prototype]] 连接(遍历构造函数的 prototype 连接到空对象的 proto 上);
  3. 这个对象会被绑定到函数调用的 this(使用 call、apply 来改变构造函数中的 this,并在 new 的阶段执行);
  4. 如果函数没有返回其它对象,那么 new 表达式中的函数调用会自动返回这个新对象(判断构造函数有没有返回值)。

同时,我们来回顾一下这张原型链连接图:

我们要实现一个 createNewObject 方法,让其可以达到如下效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 构造函数
function Dog(name) {
this.name = name;
}
Dog.prototype.sayHi = function () {
console.log(`Hi~, my name is ${this.name}`);
};
Dog.prototype.age = 18;

// 实例化
const newDog = createNewObject(Dog, ["dabai"]);
newDog.sayHi(); // 输出:Hi~, my name is dabai
console.log(newDog.age); // 输出:18

实现1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function createNewObject(constructor, args) {
const result = new Object(); // (1)
const returnResult = constructor.apply(result, args);
// 构造函数没有返回值才返回构造对象
if (returnResult instanceof Object) {
return returnResult;
}
// 标明构造器
result.__proto__ = { constructor };
Object.keys(constructor.prototype).forEach((prototypeKey) => {
const targetPrototype = constructor.prototype[prototypeKey];
// 防止实例化的对象通过 __proto__ 来修改构造函数的 prototype
Object.defineProperty(result.__proto__, prototypeKey, {
writable: false,
value: targetPrototype,
});
});
return result;
}

(1) 其实在创建一个空对象时候,可以写为 result = {} 或者 result = new Object(),甚至如果你想创建一个真正意义上的纯空的对象的话,可以使用 result = Object.create(null),但是要注意的是 Object.create(null) 的方法在 Nodejs 环境下,创建的对象无法改写 proto,这就导致我们无法去连接构造函数的 prototype。

实现2:

我们其实可以利用 Object.create() 来实现,其含义为:

Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的proto

那么实现一个 new 方法就可以改写为:

1
2
3
4
5
6
7
8
function createNewObject(constructor, args) {
const result = Object.create(constructor.prototype);
const returnResult = constructor.apply(result, args);
if (returnResult instanceof Object) {
return returnResult;
}
return result;
}

2. 实现 call apply 方法

https://github.com/mqyqingfeng/Blog/issues/11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Function.prototype.call2 = function (context) {
var context = context || window;
context.fn = this;

var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}

var result = eval('context.fn(' + args +')');

delete context.fn
return result;
}

// 测试一下
var value = 2;

var obj = {
value: 1
}

function bar(name, age) {
console.log(this.value);
return {
value: this.value,
name: name,
age: age
}
}

bar.call2(null); // 2

console.log(bar.call2(obj, 'kevin', 18));

3. bind 的实现

https://www.cnblogs.com/echolun/p/12178655.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Function.prototype.bind_ = function (obj) {
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
};
var args = Array.prototype.slice.call(arguments, 1);
var fn = this;
//创建中介函数
var fn_ = function () {};
var bound = function () {
var params = Array.prototype.slice.call(arguments);
//通过constructor判断调用方式,为true this指向实例,否则为obj
fn.apply(this.constructor === fn ? this : obj, args.concat(params));
console.log(this);
};
fn_.prototype = fn.prototype;
bound.prototype = new fn_();
return bound;
};

4. 实现一个深克隆

https://juejin.cn/post/6844903929705136141#heading-8

重点:

  1. 使用递归实现深拷贝
  2. 基础要实现 Object 和 Array 的拷贝
  3. 创建一个 Map 来存放已经拷贝过的对象,防止循环引用
  4. 使用 Object.prototype.toString 来判断拷贝对象的
    1. 对于 Map、Set 要遍历拷贝
    2. 对于 Boolean、Number、String、Error 要调用对应的构造函数来拷贝
    3. 对于 RegExp 和 Symbol 要单独特殊处理
    4. 对于函数来说,可以通过 toString 将函数转为字符串,然后使用目标函数是否有 prototype 来判断其是箭头函数还是普通函数:
      1. 箭头函数可以直接返回 eval(functionString) 的执行结果来拷贝函数
      2. 对于普通函数,需要利用正则解析函数的参数位以及函数体,再利用 new Function(…paramArr, functionBody) 来克隆一个函数

JSON.stringify 的局限性:

  • 仅能正确克隆基础类行,以及克隆对象、数组
  • 对于无法拷贝的对象(这些对象通常在 JSON 中没有有效概念),如:Map、Set、RegExp、Function,会返回一个空对象 {}
  • 对于 undefined 会直接忽略该键值
  • 会将 NaN 转为 null
  • 会将 Date 转为时间字符串
  • 无法序列化的对象,如 BigNumber,会直接报错
  • 对于循环引用会报错

基础版(仅实现了 1~3):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function clone(target, map = new Map()) {
if (typeof target === 'object') {
let cloneTarget = Array.isArray(target) ? [] : {};
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
for (const key in target) {
cloneTarget[key] = clone(target[key], map);
}
return cloneTarget;
} else {
return target;
}
};

5. 实现继承

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create

ES5 环境下,可以使用 SubClass.prototype = Object.create(ParentClass) 来连接父类的原型链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Shape - 父类(superclass)
function Shape() {
this.x = 0;
this.y = 0;
}

// 父类的方法
Shape.prototype.move = function(x, y) {
this.x += x;
this.y += y;
console.info('Shape moved.');
};

// Rectangle - 子类(subclass)
function Rectangle() {
Shape.call(this); // call super constructor.
}

// 子类续承父类
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

var rect = new Rectangle();

console.log('Is rect an instance of Rectangle?',
rect instanceof Rectangle); // true
console.log('Is rect an instance of Shape?',
rect instanceof Shape); // true
rect.move(1, 1); // Outputs, 'Shape moved.'

如果你希望能继承到多个对象,则可以使用混入的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function MyClass() {
SuperClass.call(this);
OtherSuperClass.call(this);
}

// 继承一个类
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;

MyClass.prototype.myMethod = function() {
// do a thing
};

更低版本的 ES 标准下,可以使用 new 关键字的特性来模拟 Object.create

1
2
3
4
5
function objectCreate(o) {
function F(){};
F.prototype = o;
return new F();
}