手写代码系列

手写代码系列

三月 10, 2021

手写一个instanceof

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
function myInstanceof (obj, target) {
let res = false;
let tempObj = obj;
while(tempObj.__proto__) {
if(tempObj.constructor === target) {
res = true;
break;
}
tempObj = tempObj.__proto__;
}
return res;
}

class Animal {}

class Dog extends Animal {}

const husky = new Dog();

console.log(husky instanceof Animal); // true

console.log(myInstanceof(husky, Animal)) // true

console.log(Animal instanceof Animal); // false

console.log(myInstanceof( Animal, Animal )) // false

__proto__可以用Object.getPrototypeof()代替。因为前者不是标准方法,是浏览器自己实现的。

手写斐波那契数列

很经典的问题,斐波那契数列的意思是:第一位为1、第二位为1、第n位为前两项之和。

递归

所以最符合思路的就是递归。

1
2
3
4
5
6
function fibo(n) {
if (n === 1) return 1;
if (n === 2) return 1;
// 结果是前两项之和。
return fibo(n - 2) + fibo(n - 1);
}

然后就会扯到、递归的危害和优化,危害当然是堆栈过多。而且有个规律是,尾递归是肯定可以优化的。以上代码就是个经典的尾递归。就是递归方法的调用在方法的最后。

尾递归为啥能优化?

函数栈的目的是啥?是保持入口环境。那么在什么情况下可以把这个入口环境给优化掉?答案不言而喻,入口环境没意义的情况下为啥要保持入口环境?尾递归,就恰好是这种情况。

尾递归保存的状态再递归执行完成后立马被返回,没有意义。

循环

一般思路下,尾递归可以用循环的方式替代

而这就没必要用什么固定思路去转换了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function fibo(n) {
if (n === 1) return 1;
if (n === 2) return 1;

let sum = 0;
let n1 = 1;
let n2 = 1;

for (let i = 2; i < n; i++) {

sum = n1 + n2;

n1 = n2;
n2 = sum;

}

return sum;

}

思路是。一个for循环,sum值是前两项的和,则需要两个变量存储前两项的和。n1、n2

每次i递增。则把n1丢弃、n1 = n2、n2 = sum即可。

手写bind

只考虑普通函数的情况时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Function.prototype.myBind = function(context, ...args) {
const fn = this;
// bind返回的方法
return function(...innerArgs) {
return fn.call(context, ...args, ...innerArgs)
};
}

function print(...inputs) {
console.log(...inputs)
}

// const printAbc = print.bind(null, 'abc')
const printAbc = print.myBind(null, 'abc')
printAbc('def'); // 'abc' 'def'

普通函数时很简单

  • 返回一个新函数
  • 新函数的执行上下文固定是bind方法的第一个参数
  • bind方法除了第一个参数,作为新函数的默认参数

以上简简单单几行,利用call指定执行上下文即可

手写call、apply

有几个小点是要注意的:

  1. 没有传递context时,context默认时window
  2. 在context环境下调用function
  3. 调用完记得删除context环境下的function
  4. 进阶一点的时,要防止ctx.fn不会覆盖ctx本身的内容,一个简单的方法是,用Symbol来命名fn。
1
2
3
4
5
6
7
8
9

Function.prototype.myCall = function(context, ...args) {
const ctx = context || window;
ctx.fn = this;
const result = ctx.fn(...args);
delete ctx.fn;
return result;
}

apply同理

1
2
3
4
5
6
7
8
9

Function.prototype.myapply = function(context, args = []) {
const ctx = context || window;
ctx.fn = this;
const result = ctx.fn(...args);
delete ctx.fn;
return result;
}

用new操作符

先复习new操作符干了啥

  1. 新建一个空白对象
  2. 将这个空对象的原型指向构造函数的prototype属性
  3. 将步骤1新创建的对象作为this的上下文
  4. 执行构造函数内部的代码
  5. 如果该函数返回值是引用类型数据,则返回该值,否则返回this

new操作也修改了this的指向。实际上,最终的this指向会已new操作符为主。

new操作符还会把构造函数的prototype赋值给新对象。所以需要以下改造

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
Function.prototype.myBind = function(context, ...args) {
const fn = this;

// 返回的方法。可能被直接调用或者被new 调用
const newFunc = function(...innerArgs) {
// 以下判断是否是由new操作符调用的该方法。
// 如果是普通调用,this应该为window,或其他执行上下文。
// 如果是new 则this则是new过程新建的对象,也就是实例。
// 所以 this instanceof newFunc会成立
if (this instanceof newFunc) {
return fn.call(this, ...args, ...innerArgs)
}
return fn.call(context, ...args, ...innerArgs)
}

// 应该修改原型链,不改,则新方法的constructor指向“newFunc”,
// 应该把其指向this.prototype,也就是Persion
newFunc.prototype = this.prototype;
return newFunc
}

function Person(name) {
this.name = name;
}

const BindedPerson = Person.myBind({name: 'name'});

const bindedMing = new BindedPerson();

console.log(bindedMing) // {name: 'ming'}

手写call

本至上,是修改了指定方法的执行上下文,然后执行,然后返回其结果。唯一要注意的是,call的第一个参数未falsy,也就是没有指定上下文,则这个上下文是window

1
2
3
4
5
6
7
8
9
10
11
Function.prototype.myCall = function(context, ...args) {
  const fn = this;
  const ctx = context || window; // 注意不传context时,默认是window

  context.fn = fn;
  const result = context.fn(...args); // 本质上就是在指定上下文下调用方法
  delete context.fn;

  return result;

}

可能存在一个问题是,context下本来就有fn方法,这么实现会导致原方法被覆盖。可以用Symble来实现唯一的key

1
2
3
4
5
6
7
8
9
10
11
12
13
Function.prototype.myCall = function(context, ...args) {
  const fn = this;
  const ctx = context || window; // 注意不传context时,默认是window

const fnName = Symble('tempFn')

  context[fnName] = fn;
  const result = context[fnName](...args); // 本质上就是在指定上下文下调用方法
  delete context[fnName];

  return result;

}