jsliang 求职系列 - 14 - 手写源码大杂烩

1,862 阅读8分钟

手写系列的代码,较为重要/内容较多的,都抽取到单独篇章去了,下面看大杂烩,概率会出的手写题目。

一 目录

不折腾的前端,和咸鱼有什么区别

目录
一 目录
二 自定义事件
2.1 创建自定义事件
2.2 事件的监听
2.3 事件的触发
2.4 案例
三 Object.create()
四 ES5 实现类继承
五 instanceof
六 柯里化
七 迭代器
八 Ajax
九 数组扁平化
9.1 方法一:手撕递归
9.2 方法二:flat()
9.3 方法三:reduce
十 对象扁平化
十一 数组去重
11.1 方法一:手撕去重
11.2 方法二:Set
11.3 方法三:filter
十二 其他
十三 参考文献

二 自定义事件

返回目录

面试官:手写一个自定义原生事件。

简单三步曲:

  1. 创建自定义事件:const myEvent = new Event('jsliangEvent')
  2. 监听自定义事件:document.addEventListener(jsliangEvent)
  3. 触发自定义事件:document.dispatchEvent(jsliangEvent)

简单实现:

window.onload = function() {
  const myEvent = new Event('jsliangEvent');
  document.addEventListener('jsliangEvent', function(e) {
    console.log(e);
  })
  setTimeout(() => {
    document.dispatchEvent(myEvent);
  }, 2000);
};

页面 2 秒后自动触发 myEvent 事件。

2.1 创建自定义事件

返回目录

创建自定义事件的 3 种方法:

  • 使用 Event
let myEvent = new Event('event_name');
  • 使用 customEvent(可以传参数)
let myEvent = new CustomEvent('event_name', {
  detail: {
    // 将需要传递的参数放到这里
    // 可以在监听的回调函数中获取到:event.detail
  }
});
  • 使用 document.createEvent('CustomEvent')initEvent()
// createEvent:创建一个事件
let myEvent = document.createEvent('CustomEvent'); // 注意这里是 CustomEvent

// initEvent:初始化一个事件
myEvent.initEvent(
  // 1. event_name:事件名称
  // 2. canBubble:是否冒泡
  // 3. cancelable:是否可以取消默认行为
)

2.2 事件的监听

返回目录

自定义事件的监听其实和普通事件一样,通过 addEventListener 来监听:

button.addEventListener('event_name', function(e) {})

2.3 事件的触发

返回目录

触发自定义事件使用 dispatchEvent(myEvent)

注意,这里的参数是要自定义事件的对象(也就是 myEvent),而不是自定义事件的名称(myEvent

2.4 案例

返回目录

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>自定义事件</title>
</head>
<body>
  <button class="btn">点我</button>
  
  <script>
    window.onload = function() {
      // 方法 1
      const myEvent = new Event('myEvent');

      // 方法 2
      // const myEvent = new CustomEvent('myEvent', {
      //   detail: {
      //     name: 'jsliang',
      //   },
      // });

      // 方法 3
      // const myEvent = document.createEvent('CustomEvent');
      // myEvent.initEvent('myEvent', true, true);

      const btn = document.querySelector('.btn');
      btn.addEventListener('myEvent', function(e) {
        console.log(e);
      })
      setTimeout(() => {
        btn.dispatchEvent(myEvent);
      }, 2000);
    };
  </script>
</body>
</html>

上面 console.log(e) 输出:

/*
  CustomEvent {
    bubbles: true
    cancelBubble: false
    cancelable: true
    composed: false
    currentTarget: null
    defaultPrevented: false
    detail: null
    eventPhase: 0
    isTrusted: false
    path: (5) [button.btn, body, html, document, Window]
    returnValue: true
    srcElement: button.btn
    target: button.btn
    timeStamp: 16.354999970644712
    type: "myEvent"
  }
*/

三 Object.create()

返回目录

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

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

试验一下:

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

const Father = function() {
  this.bigName = '爸爸';
};
Father.prototype.sayHello = function() {
  console.log(`我是你${this.bigName}`);
}

const Child = function() {
  Father.call(this);
  this.smallName = '儿子';
}
Child.prototype = create(Father.prototype);
Child.prototype.constructor = Child;

const child = new Child();
child.sayHello(); // 我是你爸爸

下面讲寄生组合式继承会用到 Object.create()

四 ES5 实现类继承

返回目录

使用 ES5 实现继承,简要在 3 行代码:

  1. Father.call(this)。在 Child 中通过 Father.call(this),将 Fatherthis 修改为 Childthis
  2. Child.prototype = Object.create(Father.prototype)。将 Child 的原型链绑定到 Father 的原型链上。
  3. Child.prototype.constructor = Child。这个构造函数的实例的构造方法 constructor 指向自身。
const Father = function (name, like) {
  this.name = name;
  this.like = like;
  this.money = 10000000;
};

Father.prototype.company = function() {
  console.log(`${this.name}${this.money} 元`);
}

const Children = function (name, like) {
  Father.call(this);
  this.name = name;
  this.like = like;
}

Children.prototype = Object.create(Father.prototype);
Children.prototype.constructor = Children;

const jsliang = new Children('jsliang', '学习');
console.log(jsliang); // Children {name: "jsliang", like: "学习", money: 10000000}
jsliang.company(); // jsliang 有 10000000 元

需要注意 Child.prototype = Object.create(Father.prototype) 这句话:

  1. 这一步不用 Child.prototype = Father.prototype 的原因是怕共享内存,修改父类原型对象就会影响子类
  2. 不用 Child.prototype = new Parent() 的原因是会调用 2 次父类的构造方法(另一次是 call),会存在一份多余的父类实例属性
  3. Object.create 是创建了父类原型的副本,与父类原型完全隔离

最后,这种继承方法,叫做 寄生组合式继承

五 instanceof

返回目录

面试官:手写一个 instanceof

其实 instanceof 就是查找原型链的过程,如果你不懂原型和原型链,去看 jsliang 的原型和原型链文章先吧:

OK,那么有下面代码:

const Father = function() {
  this.bigName = '爸爸';
};
Father.prototype.sayHello = function() {
  console.log(`我是你${this.bigName}`);
}

const Child = function() {
  Father.call(this);
  this.smallName = '儿子';
}
Child.prototype = Object.create(Father.prototype);
Child.prototype.constructor = Child;

const child = new Child();
child.sayHello(); // 我是你爸爸

console.log(child instanceof Child); // true
console.log(child instanceof Father); // true
console.log(child instanceof Object); // true

如何改造当中的 instanceof 呢?

function instanceOf(a, b) {
  let proto = a.__proto__;
  const prototype = b.prototype;

  // 从当前 __proto__ 开始查找
  while (proto) {
    
    // 如果找到 null 还没有找到,返回 false
    if (proto === null) {
      return false;
    }

    // 如果 a.__proto__.xxx === b.prototype,返回 true
    if (proto === prototype) {
      return true;
    }

    // 进一步迭代
    proto = proto.__proto__;
  }
}

console.log(instanceOf(child, Child)); // true
console.log(instanceOf(child, Father)); // true
console.log(instanceOf(child, Object)); // true

输出结果同 instanceof 一样,完成目标!

才怪!!!

经过测试:

let num = 123;
console.log(num instanceof Object); // false
console.log(instancOf(123, Object)); // true

为什么呢?因为 instanceof 在原生代码上,实际是做了基本类型的检测,基本类型应该返回 false,所以可以进行改造:

function instanceOf(a, b) {
  // 新增:通过 typeof 判断基本类型
  if (typeof a !== 'object' || b === null) {
    return false;
  }

  // 新增:getPrototypeOf 是 Object 自带的一个方法
  // 可以拿到参数的原型对象
  let proto = Object.getPrototypeOf(a);
  const prototype = b.prototype;

  while (proto) {
    if (proto === null) {
      return false;
    }
    if (proto === prototype) {
      return true;
    }
    proto = proto.__proto__;
  }
}

六 柯里化

返回目录

实现一个 add 方法,使计算结果能够满足以下预期:

add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

实现方法:

function add () {
  const numberList = Array.from(arguments);

  // 进一步收集剩余参数
  const calculate = function() {
    numberList.push(...arguments);
    return calculate;
  }

  // 利用 toString 隐式转换,最后执行时进行转换
  calculate.toString = function() {
    return numberList.reduce((a, b) => a + b, 0);
  }

  return calculate;
}

// 实现一个 add 方法,使计算结果能够满足以下预期
console.log(add(1)(2)(3)); // 6
console.log(add(1, 2, 3)(4)); // 10;
console.log(add(1)(2)(3)(4)(5)); // 15;

详细看 JavaScript 系列的闭包篇章,里面有讲解到闭包和柯里化。

七 迭代器

返回目录

迭代器的意思是:我的版本是可控的,你踢我一下,我动一下。

// 在数据获取的时候没有选择深拷贝内容
// 对于引用类型进行处理会有问题
// 这里只是演示简化了一点
function Iterdtor(arr) {
  let data = [];

  if (!Array.isArray(arr)) {
    data = [arr];
  } else {
    data = arr;
  }

  let length = data.length;
  let index = 0;

  // 迭代器的核心 next
  // 当调用 next 的时候会开始输出内部对象的下一项
  this.next = function () {
    let result = {};
    result.value = data[index];
    result.done = index === length - 1 ? true : false;
    if (index !== length) {
      index++;
      return result;
    }

    // 当内容已经没有了的时候返回一个字符串提示
    return 'data is all done';
  };
}

const arr = [1, 2, 3];

// 生成一个迭代器对象
const iterdtor = new Iterdtor(arr);
console.log(iterdtor.next()); // { value: 1, done: false }
console.log(iterdtor.next()); // { value: 2, done: false }
console.log(iterdtor.next()); // { value: 2, done: true }
console.log(iterdtor.next()); // data is all done

八 Ajax

返回目录

通过 Promise 实现 ajax

index.json

{
  "name": "jsliang",
  "age": 25
}

index.js

const getData = (url) => {
  return new Promise((resolve, reject) => {
    // 设置 XMLHttpRequest 请求
    const xhr = new XMLHttpRequest();

    // 设置请求方法和 url
    xhr.open('GET', url);

    // 设置请求头
    xhr.setRequestHeader('Accept', 'application/json');

    // 设置请求的时候,readyState 属性变化的一个监控
    xhr.onreadystatechange = (res) => {

      // 如果请求的 readyState 不为 4,说明还没请求完毕
      if (xhr.readyState !== 4) {
        return;
      }

      // 如果请求成功(200),那么 resolve 它,否则 reject 它
      if (xhr.status === 200) {
        resolve(xhr.responseText);
      } else {
        reject(new Error(xhr.responseText));
      }
    };

    // 发送请求
    xhr.send();
  })
};
getData('./index.json').then((res) => {
  console.log(res); // { "name": "jsliang", "age": 25 }
})

补充:Ajax 状态

  • 0 - 未初始化。尚未调用 open() 方法
  • 1 - 启动。已经调用 open() 方法,但尚未调用 send() 方法。
  • 2 - 发送。已经调用 send() 方法,但尚未接收到响应。
  • 3 - 接收。已经接收到部分响应数据。
  • 4 - 完成。已经接收到全部响应数据,而且已经可以在客户端使用了。

九 数组扁平化

返回目录

9.1 方法一:手撕递归

返回目录

const jsliangFlat = (arr) => {
  // 1. 设置空数组
  const result = [];

  // 2. 设置递归
  const recursion = (tempArr) => {
    // 2.1 遍历数组
    for (let i = 0; i < tempArr.length; i++) {
      // 2.2 如果数组里面还是一个数组,那么递归它
      if (Array.isArray(tempArr[i])) {
        recursion(tempArr[i]);
      } else { // 2.3 否则添加它
        result.push(tempArr[i]);
      }
    }
  };
  recursion(arr);

  // 3. 返回结果
  return result;
};

console.log(jsliangFlat([1, [2, [3, [4, [5]]]]])); // [1, 2, 3, 4, 5]

9.2 方法二:flat()

返回目录

flat 方法可以扁平数组,如果不传参数,flat() 扁平一层,flat(2) 扁平 2 层,到 flat(Infinity) 扁平所有层。

const jsliangFlat = (arr) => {
  return arr.flat(Infinity);
};

console.log(jsliangFlat([1, [2, [3, [4, [5]]]]])); // [1, 2, 3, 4, 5]

注意这个方法在 Node 执行会报错,这是一个 ES6 的方法。

9.3 方法三:reduce

返回目录

不推荐 reduce,我怕小伙伴看得头晕。

const jsliangFlat = (arr = []) => {
  return arr.reduce((prev, next) => {
    if (Array.isArray(next)) {
      return prev.concat(jsliangFlat(next));
    } else {
      return prev.concat(next);
    }
  }, [])
};

console.log(jsliangFlat([1, [2, [3, [4, [5]]]]])); // [1, 2, 3, 4, 5]

十 对象扁平化

返回目录

其实我也不知道这个有没考,也不是很难:

const obj = {
  a: {
    b: {
      c: 1,
      d: 2,
    },
    e: 3,
  },
  f: {
    g: 4,
    h: {
      i: 5,
    },
  },
};
// 1. 设置结果集
const result = [];

// 2. 递归
const recursion = (obj, path = []) => {
  // 2.1 如果到底部,此时 obj 是对应的值
  if (typeof obj !== 'object') {
    // 2.1.1 结果集加上这个字段
    result.push({
      [path.join('.')]: obj,
    })
    // 2.1.2 终止递归
    return;
  }

  // 2.2 遍历 obj 对象
  for (let i in obj) {

    // 2.2.1 判断对象自身是否含有该字段(排除原型链)
    if (obj.hasOwnProperty(i)) {

      // 2.2.2 回溯,添加路径
      path.push(i);

      // 2.2.3 进一步递归
      recursion(obj[i], path);

      // 2.2.4 回溯,删除路径,方便下一次使用
      path.pop();
    }
  }
};
recursion(obj);

// 3. 返回结果
console.log(result);
/*
[
  { 'a.b.c': 1 },
  { 'a.b.d': 2 },
  { 'a.e': 3 },
  { 'f.g': 4 },
  { 'f.h.i': 5 },
]
*/

还有反向推题:

  • 根据 obj 和路径 patha.b.c),找到它的值

递归一下就行了,或者迭代也可以,不难。

写不出来的小哥反省下,写不出来的小姐姐找我,教你啊~ /手动狗头防暴力

十一 数组去重

返回目录

看着本文标题是 3 种,实际上有 5 种。

11.1 方法一:手撕去重

返回目录

const jsliangSet = (arr) => {
  // 设置结果
  const result = [];

  // 遍历数组
  for (let i = 0; i < arr.length; i++) {
    // 如果结果集不包含这个元素
    // 这里也可以用 result.indexOf(arr[i]) === -1
    // 或者 arr.lastIndexOf(arr[i]) === i
    if (!result.includes(arr[i])) {
      result.push(arr[i]);
    }
  }

  // 返回结果
  return result;
};

console.log(jsliangSet([1, 1, 1, 2, 2])); // [1, 2]

11.2 方法二:Set

返回目录

const jsliangSet = (arr) => {
  return [...new Set(arr)];
};

console.log(jsliangSet([1, 1, 1, 2, 2])); // [1, 2]

11.3 方法三:filter

返回目录

同样,通过 filter 也可以,其实内核也是 lastIndexOf 和当前索引值的一个比对。

const jsliangSet = (arr) => {
  return arr.filter((item, index) => {
    return arr.lastIndexOf(item) === index;
  })
};

console.log(jsliangSet([1, 1, 1, 2, 2])); // [1, 2]

十二 其他

返回目录

其他的还有:

  • 发布订阅模式:Node 回调函数、Vue event bus
  • 异步并发数限制
  • 异步串行|异步并行
  • 图片懒加载
  • 滚动加载
  • 数组 API 实现:filtermapforEachreduce
  • 大数据渲染(渲染几万条数据不卡页面)
  • JSON:JSON.parse()JSON.stringify()

但是 jsliang 可能面试见得少,我就不贴出来了,如果小伙伴们发现某个手写代码出现的挺频繁的,欢迎评论留言吐槽,jsliang 抽空将其写出来。

十三 参考文献

返回目录


jsliang 的文档库由 梁峻荣 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议 进行许可。
基于 github.com/LiangJunron… 上的作品创作。
本许可协议授权之外的使用权限可以从 creativecommons.org/licenses/by… 处获得。