JS 手撕源码

390 阅读12分钟

1. new

new命令执行时的具体步骤:

  1. 创建一个空对象,并将空对象的原型指向构造函数的 prototype 属性

  2. 将这个空对象赋值给构造函数内部的 this,然后执行函数。

  3. 返回对象:如果构造函数内部返回了一个非空对象,那么new命令应该会返回这个非空对象,否则就返回 this对象

// 借助 apply 来执行函数
function myNew(constructor, ...args) {
  let obj = {};
  Object.setPrototypeOf(obj, constructor.prototype);
  let res = constructor.apply(obj, args);
  return (typeof res === 'object' && res !== null) ? res : obj;
}
// 测试
function Person(name, age) {
  this.name = name;
  this.age = age;
}
let actor = myNew(Person, '张三', 28);
console.log(actor.age);  // 28

2. instanceof

instanceof 返回一个布尔值,表示对象是否为构造函数的实例。原理是:它会检查后面构造函数的prototype属性 是否在 前面实例对象的原型链上

应用场景: 判断数据类型、判断对象是否在原型链上。

// 模拟 lt instanceof rt
function isInstanceof(lt, rt) {
   // 除数组、函数、对象(除了null)以外的类型,直接返回 false
   if (lt === null || (typeof lt !== 'object' && typeof lt !== 'function')) {
        return false;
   }
   
  lt = lt.__proto__;
  while(lt) {
    if (lt === rt.prototype) return true;
    lt = lt.__proto__;
  }
  return false;
}

3. call/apply/bind

call/apply/bind

4. 函数的柯里化

数学中的柯里化是一种将使用多个参数的一个函数转换成多个使用一个参数的函数的技术。

但在 JS 中,柯里化函数可以提前接收多个参数,然后返回一个函数接收剩余参数, 当函数接收的参数数量与原函数的形参数量相等时执行原函数

func.length:形参个数,arguments:实参。

// 用柯里化实现累加器
function sum (a) {  
  return function (b, c) {
    console.log(a + b * c);
  }
}
sum(2)(3, 4);  // 14

柯里化的用途:主要服务于函数式编程,可以延迟计算、固定参数

(1)固定的参数 toCurry(fn, x)(y, z)

// 将 fn 函数柯里化,返回的是一个经过柯里化的函数
function toCurry(fn, ...args) {
  return function() {
    let params = [...args, ...arguments];
    if (params.length < fn.length) {  // 判断实参数量是否等于形参数量
      return toCurry(fn, ...params);  // 注意return,不够就继续递归
    } else {
      return fn(...params);
    }
  }
}

测试用例

function ops(a, b, c) {
  console.log(a + b + c);
}
toCurry(ops, 1)(2)(3);  // 6

(2)不定长的参数

定义一个函数满足如下,且只有调用count()时才能执行
add(1).count() === 1
add(1)(2).count() === 3
add(1)(2)(3).count() === 6

5. 深浅拷贝

深拷贝、浅拷贝

6. 闭包自增

// 写法一
function  counterCreator(){   
  let index = 1;  
  return function counter(){  // 闭包函数
      console.log(index++);
  } 
}

// 写法二
function  counterCreator(){   
  let index = 1;  
  function counter(){  // 闭包函数
      console.log(index++);
  }
  return counter;   
}
// 测试
let counterA = counterCreator();  
let counterB = counterCreator();
counterA();  //1
counterA();  //2,闭包可以记住它的执行环境
counterB();  //1
counterB();  //2

7. Array.prototype.reduce:要有 return

reduce从左向右依次处理数组的每个成员,累计为一个值reduceRight则从右向左。

[1, 2, 3, 4].reduce(function(a, b) {
  return a + b;  // return
}, 10);  
// 20
  • 第 1 个参数是 函数,该函数接受 4 个参数:
    • 累积变量(必须):第一次执行时,默认为数组的第一个成员;以后每次执行都是上一轮的返回值
    • 当前变量(必须):第一次执行时,默认为数组的第二个成员;以后每次执行都是下一个成员
    • 当前位置(可选):表示 当前变量(第2个参数) 的位置,默认 1。
    • 原数组
  • 第 2 个参数是 给累积变量指定初值(可选,建议加上)。如果指定了初值,那么函数的第2个参数(当前变量)是从数组的第一个成员开始遍历

源码实现:

Array.prototype.myReduce = function(fn, val) {
  let num = val ? val : this[0];  // 初始化函数的第 1 个参数 - 累计变量
  let index = val ? 0 : 1;  // 初始化函数的第 3 个参数 - "当前变量"的位置
  // this 是原数组
  for (let i = index; i < this.length; i++) {
    num = fn(num, this[i], i, this);  // 传入 4 个参数
  }
  return num;
}

8. Array.prototype.map:要有 return

依次将数组所有成员传入参数函数,然后将执行结果组成一个新数组返回,不改变原数组。

map 会跳过数组的空位,但不会跳过 nullundefined

[1, 2, 3, 4].map(function(x) {
  return x + 1;  // return
});  
// [2, 3, 4, 5]
  • 第 1 个参数是函数,该函数接受 3 个参数:(reduce 的 后 3 个参数)
    • 当前成员
    • 当前位置
    • 原数组
  • 第 2 个参数,用来绑定回调函数内部的 this变量

源码实现:

Array.prototype.myMap = function(fn, thisValue) {
  const res = [];
  for (let i = 0; i < this.length; i++) { // this 就是调用的数组
    let val = fn.call(thisValue, this[i], i, this);   // 注意 call
    res.push(val);
  }
  return res;
}

// 测试
[1, 2].myMap(function(x) {
  return this[x];  // 有了 this,不能用箭头函数 
}, ['a', 'b', 'c']);
// ['b', 'c']

9. Array.prototype.forEach:无 return

forEach()map() 很相似,而且也会跳过数组的空位,不会跳过 null 和 undefined区别在于:forEach() 不返回值,只是用来操作数据;而map要有返回值。

Array.prototype.myForEach = function(fn, thisValue) {
  for (let i = 0; i < this.length; i++) {
    fn.call(thisValue, this[i], i, this);
  }
}

另外,forEach()、map() 等无法中断执行,总是会将所有成员都遍历完,break、continue都报错。如果希望能控制中断执行,建议使用 for 循环

10. Array.prototype.filter:要有 return

filter()用于过滤数组成员,将满足条件的成员组成一个新数组返回。

  • 第 1 个参数是函数,该函数接受 3 个参数:(同 map
    • 当前成员。
    • 当前位置。
    • 原数组。
  • 第 2 个参数,用来绑定回调函数内部的 this变量

源码实现

Array.prototype.myFilter = function(fn, thisValue) {
  const res = [];
  for (let i = 0; i < this.length; i++) { // this 就是调用的数组
    let flag = fn.call(thisValue, this[i], i, this);   // 返回布尔值
    if (flag) res.push(this[i]);  // 与 map 唯一区别
  }
  return res;
}

11. Array.prototype.indexOf:严格相等

返回给定元素在数组中 第一次出现的位置,没有则返回 -1lastIndexOf() 返回最后一次出现的位置。

  • 第 1 个参数:要查找的元素。
  • 第 2 个参数:表示开始搜索的位置

不能用来搜索 NaN,因为内部使用严格相等运算符,而NaN !== NaN

Array.prototype.myIndexOf = function(val, index = 0) {
  for (let i = index; i < this.length; i++) {
    if (this[i] === val)  return i;  // 严格相等
  }
  return -1;  // 找不到返回 -1
}

12. flat(),要求可控制打平层数

ES6 新增的Array.prototype.flat() 用于将嵌套的数组拉平,返回新数组,不改变原数组

  • 可以传参,表示想要拉平的层数,默认只会“拉平”一层。
  • 不管有多少层嵌套,如果想转成一维数组,那么传参Infinity即可。
[1, 2, [3, 4]].flat();

递归: ...myFlat

function myFlat(arr, depth = 1) {
  const res = [];
  for (let item of arr) {
    if (Array.isArray(item) && depth > 0) {  // 注意 depth > 0
      res.push(...myFlat(item, depth - 1));  // 注意
    } else {
      res.push(item);
    }
  }
  return res;
}

借助reduce递归: myFlat

function myFlat(arr, depth = 1) {
  if (depth > 0) {
    arr = arr.reduce((prev, curr) => { 
      return prev.concat(Array.isArray(curr) ? myFlat(curr, depth - 1) : curr); // 注意 return
    }, []);  // 累计变量初值
  }
  return arr;
}

要求:如果没传depth就拉成一维,如果传了depth就按depth

function myFlat(arr, depth) {
  depth = (arguments[1] || arguments[1] === 0 ) ? arguments[1] : Infinity;
  if (depth === Infinity) {}
  else {}

13. repeat(函数,执行次数,间隔):闭包

function repeat(fn, times, delay) {
  return function(str) {  // 返回一个函数
    for (let i = 0; i < times; i++) {
      setTimeout(() => {
        fn(str);
      }, i * delay);
    }
  }
}

// 测试
const repeatFunc = repeat(console.log, 4, 2000);
repeatFunc('hello world');

14. 防抖节流:闭包

函数防抖和函数节流都是为了解决某个事件在短时间内频繁触发的问题。防抖是延迟执行,以最后一次事件触发为准;节流是间隔执行,以单位时间内的第一次事件触发为准。

  1. 防抖 debounce :在事件被触发 n 秒执行回调函数,如果在这 n 秒内事件又被触发,则会重新计时。

debounce的调用方式是 dom元素.onMouseMove = debounce(fn, delay)对 fn 进行防抖,delay 时间后执行回调函数 fn。

当第一次触发 MouseMove 事件后,会执行 debounce 函数,此时会(1)创建一个定时器(2)返回一个闭包函数。类似闭包的自增函数,如果 delay 时间内又触发 MouseMove 事件,那么此时执行的就是 返回的闭包函数,而不是 debounce 函数,也就是会跳过let timer = null这句。

function debounce(fn, delay) {
  let timer = null;  // 声明定时器
  
  return function() {  // 返回一个闭包函数
    let context = this; 
    const args = [...arguments]; 
    
    timer && clearTimeout(timer);  // 如果再次触发,则取消定时器
    timer = setTimeout(function() {  // 开始计时 或 重新开始计时
      fn.apply(context, args);
    }, delay);
  };
}

应用场景:

  • 不断地调整浏览器窗口大小会频繁触发resize事件,用防抖来让其只执行一次。
  • 搜索框搜索联想,等用户最后一次输入完再触发联想。
  1. 节流 throttle:当持续触发事件时,在规定的单位时间内只能调用一次回调函数。如果单位时间内触发了该事件,则什么也不做、也不会重置定时器
function throttle(fn, delay) {
  let timer = null;
  
  return function() {
    let context = this;
    let args = [...arguments];
    
    if (!timer) {  // 单位时间内再次触发事件时,则什么也不做
      timer = setTimeout(function() {
        fn.apply(context, args);
        timer = null;  // 注意放在延时函数中,避免异同步问题
      }, delay);
    }
  };
}

应用场景:

  • 鼠标不断点击触发,点击事件(mouseDown)单位时间内只触发一次。
  • 不断滚动文档会频繁触发scroll事件

15. Promise.all(重要)

Promise.all 基础知识

  • Promise.all()方法通常接受一个数组作为参数,但不是必须的。
  • 如果p1不是Promise实例,就会先调用Promise.resolve()方法将参数转为Promise实例。
  • 返回的有两部分:
    • p的状态fulfilledrejected
    • 要传给p的回调函数的值:其中fulfilled状态时,会将p1、p2、p3的返回值组成一个数组传递给p的回调函数;rejected状态时,会将第一个被reject的实例的返回值传递给p的回调函数。
Promise.myAll = (promises) => {
  return new Promise((resolve, reject) => {
    const res = [];
    for (let i = 0; i < promises.length; i++) {
      Promise.resolve(promises[i])  // 当参数不是Promise实例时,需要手动转化一下
      .then((data) => {
        res.push(data); // fulfilled
        if (res.length == promises.length) {
          resolve(res); // 将数组传递给 p 的then回调函数
        }
      })
      .catch(reject); // 这里只改变了`rejected`状态,没有给 p 的catch回调函数传递值
    }
  });
}

测试用例

const p1 = Promise.resolve(1);
const p2 = Promise.reject('err');
const p3 = new Promise((resolve) => {
    setTimeout(() => resolve(2), 2000);
})

const p = Promise.myAll([p1, p2, p3]) // 注意
          .then(data => console.log(data))
          .catch(err => console.log(err));

注意:参数(resolve, reject)实际是JavaScript引擎提供的两个函数。调用时可以不加括号,这时只改变了fulfilled状态、但没有给回调函数传参。比如

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done');
  });
}

16. Promise.race

const p = Promise.race([p1, p2, p3]);

只要参数中有一个实例率先改变状态,那么p的状态就跟着改变。并且将那个率先改变的Promise实例的返回值,传递给p的回调函数。

源码实现

Promise.myRace = (promises) => {
    return new Promise((resolve, reject) => {
        promises.forEach((p) => {
            Promise.resolve(p).then(resolve).catch(reject);
        });
    })
}

测试用例

const p1 = new Promise((resolve) => {
    setTimeout(() => resolve(1), 3000);
});
const p2 = new Promise((reject) => {
    setTimeout(() => reject('err'), 1000);
})
const p3 = new Promise((resolve) => {
    setTimeout(() => resolve(2), 2000);
})

const p = Promise.myRace([p1, p2, p3])
          .then(data => console.log(data))
          .catch(err => console.log(err));

17. Promise 实现 sleep,功能为延迟执行(重要)

要求实现一个sleep函数,能够先输出111,time 秒后又输出222

方式一:Promise

function sleep(time) {
  return new Promise((resolve) => {
    setTimeout(resolve, time * 1000);  // resolve是函数,传给setTimeout直接用函数名
  });
}
// 注意测试
console.log('111');
sleep(2).then(() => console.log('222'));

方式二:async / await

function sleep(time) {
  return new Promise((resolve) => {
    setTimeout(resolve, time * 1000);
  });
}
async function print(time) {
  console.log('111');
  await sleep(time);
  console.log('222');
}
print(2);

Promise实现一个sleep函数,每隔一段时间打印一个数组元素。

function sleep(time) {
  return new Promise((resolve) => {
    setTimeout(resolve, time * 1000);
  });
}

async function print(arr, time) {
  for (let item of arr) {
    await sleep(time);
    console.log(item);
  }
}

print([1, 2, 3], 2);

18. 实现"每 3s 打印一个 hello,共打印 4 次"

法一:let + setTimeout 和 i*delay

for (let i = 0; i < 4; i++) {
  setTimeout(() => console.log('hello'), i * 3000);
}

法二:计数 + setInterval + clear..

let n = 0;
let id = setInterval(() => {
  console.log('hello');
  n++;
  if (n >= 4) clearInterval(id);  
}, 3000);

法三:var + setTimeout 闭包和 i*delay

for (var i = 0; i < 4; i++) {
  (function(j) {
    setTimeout(() => console.log('hello'), j * 3000);
  })(i);
}

19. 实现倒计时效果

for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(5 - i), i * 1000);
}

20. 发布-订阅模式、单例模式

设计模式:单例模式、发布订阅模式

21. 手写寄生组合继承

寄生组合继承

22. Object.create()

该方法接受一个对象作为参数,然后以它为原型返回一个实例对象。该实例完全继承原型对象的属性。

function createObj(obj) { 
  function Func() {}   // 创建一个空的构造函数
  Func.prototype = obj;  // 让它的 prototype 属性指向参数对象 obj
  return new Func();   // 返回一个实例对象
}
// 测试
let person = {
  name : 'Jack'
};
let p1 = createObj(person);

23. 小驼峰命名法和下划线命名法

replace接受两个参数,第一个是正则表达式,表示搜索模式,第二个是要替换的内容。

  1. helloWorldCase => hello_world_case
function toLine(str) {
  return str.replace(/[A-Z]/g, (s) => '_' + s.toLowerCase());
}
  1. hello_world_case => helloWorldCase
  • 第 1 个参数的括号不能去除。
  • 第 2 个参数函数有两个括号!
function toHump(str) {
  // 第一个参数是匹配到的字符串 '_w',第二个参数是与子表达式所匹配的字符串'w'
  return str.replace(/_([a-z])/g, (line, s) => s.toUpperCase()); 
}

24. 手写 jsonp

<script src="http://api.foo.com?callback=bar"></script>

// 目标网页,返回的json数据,回调函数
function jsonp(url, data={}, callback='callback') {
  // 拼接url后面的字符串
  data.callback = callback;
  const params = [];
  for (let key in data) {
    params.push(`${key}=${data[key]}`);
  } 
  // 构造script
  let script = document.createElement('script');
  script.src = url + '?' + params.join('&');
  document.body.appendChild(script);
  // 回调函数拿到数据
  return new Promise((resolve, reject) => {
    window[callback] = (data) => {
      try {
        resolve(data);
      } catch(e) {
        reject(e);
      } finally {
        script.parentNode.removeChild(script);
      }
    }
  });
}

jsonp('http://api.foo.com', {
  page:1,
  note:'recommend'
}, 'bar')

25. XHR 实现 ajax

  • XMLHttpRequest.readyState 返回一个整数,表示当前实例的状态。4 表示服务器返回的数据已经完全接收,或者本次接收已经失败。
  • XMLHttpRequest.status表示服务器回应的状态码。基本上,只有 2xx 和 304 的状态码,表示服务器返回是正常状态。
  • open方法用于指定建立 HTTP 连接的一些细节。true 表示请求是异步的。
function ajax(url, method = 'get') {
  let xhr = new XMLHttpRequest();
  xhr.open(method, url, true);  // 参数位置
  xhr.onreadystatechange = function() {  // 指定监听函数,监听通信状态(readyState属性)的变化
    if (xhr.readyState == 4) { 
      if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) { // 请求成功
        console.log(xhr.responseText); // 接收到的字符串
      } else {
        console.log(xhr.statusText); // 服务器发送的状态提示
      }
    }
  }
  xhr.send();  // 发送请求
}

26. Promise 封装 Ajax

function ajax(url, method = 'get') {
  return new Promise((resolve, reject) => { // 改变 1
    let xhr = new XMLHttpRequest();
    xhr.open(method, url, true);
    xhr.onreadystatechange = function() {
      if (xhr.readyState == 4) {
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) { // 请求成功
          resolve(xhr.responseText); // 改变 2
        } else {
            reject(xhr.statusText); // 改变 3
        }
      }
    }
    xhr.send();
  });
}

27. 解析 url 参数,作为对象

  • 完整的 url <scheme>://<user>:<password>@<host>:<port>/<path>;<params>?<query>#<frag>
  • 输入:给定 url "http://sample.com/?a=1&b=2&c=xx&d=2#hash"
  • 输出:{ a: '1', b: '2', c: 'xx', d: '2' }
const getUrlParams = (url) => {
  const a = url.split('?').pop().split('#').shift().split('&');  // pop、shift返回值都是删除的元素
  let obj = {};
  for (let i = 0; i < a.length; i++) {
    const [key, value] = a[i].split('=');  // 解构赋值
    obj[key] = value;
  }
  return obj;
}

url说明

28. 实现HEX十六进制和RGB颜色的转换

输入#FF0000 输出 rgb(255, 0, 0)

function hexToGgb(hex) {
  return 'rgb('
        + parseInt('0x' + hex.slice(1, 3)) + ','  // 以 0x 开头的字符串会按照十六进制解析
        + parseInt('0x' + hex.slice(3, 5)) + ','
        + parseInt('0x' + hex.slice(5)) + ')';
}

输入 rgb(255, 0, 0) 输出 #FF0000

// 1. toString(进制):可以转为任意进制
// 2. '0' 和 slice(-2),用于填充 0
function rgbToHex(rgb) {
  const a = rgb.match(/\d+/g);
  return '#' + ('0' + Number(a[0]).toString(16)).slice(-2).toUpperCase() 
             + ('0' + Number(a[1]).toString(16)).slice(-2).toUpperCase()  
             + ('0' + Number(a[2]).toString(16)).slice(-2).toUpperCase();
}

29. 邮箱、手机号、日期的正则表达式

参考文章

  • 要以 [a-z]、[A-Z]、[0-9]、'-'、'_'开头,中间内容随便填写;
  • 字符串中要有@@后面要紧跟上面的字符 并且 至少匹配两次;
  • .加上 2到3个字母结尾。
/**
 * ^放在开头,用来对首字符做规定
 * +表示前面的模式匹配1次或多次
 * {2,}至少匹配2次
 * $用来规定字符串的结束位置
 */
e = '18701326275@163.com';
/^([A-Za-z0-9_-])+@([A-Za-z0-9_-]{2,})(.[A-Za-z]{2,3})$/.test(e)
// true

手机号:

/^1[3-9][0-9]{9}$/.test(e)

日期时间 :

1. yyyy-mm-dd
/**
 * ^[1-9]:年份一般第一个数字不为0
 * 月份、日期都有要求
 */
/^[1-9]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/.test(e)

2. hh:mm:ss
/^([0-1]\d|2[0-3]):[0-5]\d:[0-5]\d$/

3. 拼接后,其中 \s+:匹配一个或多个空格
/^[1-9]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])\s+([0-1]\d|2[0-3]):[0-5]\d:[0-5]\d$/

30. 扁平数据结构和json Tree结构的转换

  1. 数组 => Tree
const arr = [
  {id: 1, name: '部门1', pid: 0},
  {id: 2, name: '部门2', pid: 1},
  {id: 3, name: '部门3', pid: 1},
  {id: 4, name: '部门4', pid: 3},
];
const tree = [
  {
    id: 1,
    name: "部门1",
    pid: 0,
    children: [
      {
        id: 2,
        name: "部门2",
        pid: 1,
        children: []
      },
      {
        id: 3,
        name: "部门3",
        pid: 1,
        children: [
          {
            id: 4,
            name: "部门4",
            pid: 3,
            children: []
          }
        ]
      }
    ]
  }
]
function arrayToTree(arr) {
  const res = [];
  const map = new Map();
  for (let item of arr) {
    map[item.id] = {...item, children: []};  // 填充map,并初始化 children
    let obj = map[item.id];  // 待填充项
    if (item.pid === 0) {  // 根节点
      res.push(obj);
    } else {  // 查找父节点,并填充它的children,子元素的pid等于父元素的id
      map[item.pid].children.push(obj);  // 引用类型的传址传递
    }
  }
  return res;
}
  1. Tree => array
function treeToArray(tree, res = []) {
  for (let item of tree) {  // 针对每一层的tree
    const {children, ...props} = item;  // props: {}
    res.push(props);
    if (children.length > 0) {
      treeToArray(children, res);  // 递归
    }
  }
  return res;
}

31. dom 转为 json

<div>
  <span>
     <div></div>
     <div></div>
  </span>
  <span>
      <div></div>
      <div></div>
  </span>
</div>
转换为:
{
  tag: 'DIV',
  children: [
    {
      tag: 'SPAN',
      children:[
        { tag:'DIV', children:[] },
        { tag:'DIV', children:[] }  
      ]
    },
    {
      tag: 'SPAN',
      children:[
        { tag:'DIV', children:[] },
        { tag:'DIV', children:[] }  
      ]
    }
  ]
}
function domToJson(dom) {
  const obj = {};
  obj.name = dom.tagName;
  obj.children = [];
  dom.childNodes.forEach(c => {
    obj.children.push(domToJson(c));
  });
  return obj;
}

32. 获取树对象的属性

var tree = {
  name : '中国',
  children : [
   {
    name : '北京',
    children : [
     {
      name : '朝阳区',
      code : '0123'
     },
     {
      name : '海淀区'
     },
     {
      name : '昌平区'
     }
    ]
   },
   {
    name : '浙江省',
    children : [
     {
      name : '杭州市',
      code : '0571',
     },
     {
      name : '嘉兴市'
     },
     {
      name : '绍兴市'
     },
     {
      name : '宁波市'
     }
    ]
   }
  ]
 };
 
var node = fn(tree, '杭州市');
console.log(node);    // { name: '杭州市', code: 0571 }
// 观察对象结构
const fn = function(tree, name) {
  let queue = [tree];
  while (queue.length > 0) {
    let node = queue.shift();
    // 如果找到等于name的对象,就返回该对象
    if (node.name === name) return {name: name, code: node.code};
    // 如果有子节点,那就还要遍历
    if (node.hasOwnProperty('children')) {
      for (let item of node.children) {
        queue.push(item);
      }
    } else {  // 当没有子项,但不等于name,就继续比较下一个节点
      continue;
    }
  }
  return -1;
}
console.log(fn(tree, '杭州市'));

33. 通过字符串a.b.c获取对象的属性值

let obj = {
  a: {
    b: {
      c: 1
    }
  }
} 
find(obj, 'a.b.c')  // 1
const find = function(obj, s) {
  let a = s.split('.');
  let res = obj;
  for (let i = 0; i < a.length; i++) {
    res = res[a[i]];  // 方括号中不加引号,当作变量处理
  }
  return res;
}

34. Object.defineProperty

实现(a === 1 && a === 2 && a === 3) 等于 true

Object.defineProperty(this, 'a', {    
  get: function() {  // 当访问属性时会调用该函数,函数返回值会用作属性值 
    if (!this.value) {
      this.value = 1;
    } else {
      this.value++;
    }
    return this.value;   
  }
})

35. 并发 Promise 请求

现有10个异步请求需要发送,但是同一时刻的并发请求数量最多为3个,一个请求处理完成后才能推进下一个请求。

class Scheduler {
  constructor(maxCount) {
    this.list = [];  // 存放总的异步请求
    this.runNum = 0;  // 正在处理的异步请求个数
    this.maxCount = maxCount;  // 最大并发个数
  }
  add(requestFn) {  // 添加请求函数
    this.list.push(requestFn);
  }
  run() {  
    // 当有异步请求,且正在执行的异步请求个数小于maxNum时,才能执行下一个
    if (this.list.length && this.runNum < this.maxCount) {  
      this.runNum++;  
      let req = this.list.shift();  // 弹出一个promise函数
      req().then(() => {  // 调用p()会在delay时间后返回一个resolve的promise对象,此时异步请求执行完成
        this.runNum--;
        this.run();  // 请求完成后继续执行下一个
      });
    }
  }
  start() {  // 开始执行时先执行前三个异步请求
    for (let i = 0; i < this.maxCount; i++) {
      this.run();
    }
  }
}

// 测试
// delay时间后,promise实例状态变成resolved,并返回这个promise实例
const timeout = (delay) => new Promise((resolve) => setTimeout(resolve, delay));  
const sche = new Scheduler(3);  // 最多处理3个
const addTask = (delay, order) => {  // delay时间后,输出order
  sche.add(() => timeout(delay).then(() => console.log(order)));  // 添加的是一个函数
}
addTask(5000, 1);
addTask(3000, 2);
addTask(2000, 3);
addTask(500, 4);
sche.start();
// 3 4 2 1

36. ajax 请求超时重试

request(params, timeout, retryTimes)

  • params:ajax 请求的参数
  • timeout:每次请求之间固定的时间间隔
  • retryTimes:最大请求重试次数
function request(params, timeout, retryTimes) {
  return new Promise(function(resolve, reject) {
    // 如果 timeout 时间内 ajax 没有返回结果,p 的状态就变为 rejected
    Promise.race([ 
      ajax(...params),
      new Promise(function(resolve, reject) {
          setTimeout(reject, timeout))
      })
    ])
    .then(data => console.log(data))
    .catch(e => {
        retryTimes--;
        (retryTimes > 0) ? request(params, timeout, retryTimes) : console.log(e);
    })
  };
}

37. 输出随机的字符串

输入一个数字,输出该数字长度的随机字符串

Math.random返回一个[0, 1)之间的随机数。

1. 任意范围的随机数
function getRandom(min, max) {
  return min + Math.random() * (max - min); 
}
2. 任意范围的随机整数
function getRandom(min, max) {
  return min + Math.floor(Math.random() * (max - min + 1)); 
}
3. 随机字符
function random_str(length) {
  let ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
  ALPHABET += 'abcdefghijklmnopqrstuvwxyz';
  var str = '';
  for (var i = 0; i < length; ++i) {
    let rand = Math.floor(Math.random() * ALPHABET.length);
    str += ALPHABET.slice(rand, rand + 1);
  }
  return str;
}

38. 请求依赖

有A、B、C、D四个请求获取数据的函数(函数自己实现),每个请求的返回时间不固定。其中 C 依赖于 B 的结果作为参数,D 依赖于 ABC 的结果作为参数,最终输出 D。请给出时间最短的的算法。

智力题

  1. 先抛硬币的人优势多大:x + 1/2x = 1, x = 2/3。
  2. 老虎吃羊,奇偶数推到
  3. 天平称小球,先拿6个每端放3个。共2次。
  4. 高楼扔鸡蛋:有一栋楼共100层,一个鸡蛋从第N层及以上的楼层落下来会摔破, 在第N层以下的楼层落下不会摔破。给你2个鸡蛋,设计方案找出N,并且保证在最坏情况下, 最小化鸡蛋下落的次数。
  • 假设最小次数x,那么我们就在x层摔(这样即使碎了,从底层往上遍历的次数也是x),
    • 碎了,那么第2个鸡蛋必须从1~(x-1)层遍历
    • 没碎,那么第二次就在第x+(x-1)层摔。(因为我们假设最小摔的次数是x,之前已经摔过一次了,那么只能摔(x-1)次了)。
  • 最后得到公式x + (x - 1) + (x -2) +... + 1,使得这个值大于等于100即可,注意不能等于99,因为不确定鸡蛋在100层会不会碎。