JavaScript 高频面试手写题,你都会了吗?

867 阅读18分钟

JS 手写题目汇总

总结面试中常见的 JavaScript 手写题,比如,call、apply、bind、promise 等等,只有能够游刃有余的熟练掌握此类面试常见的 JavaScript 手写题目,才能在面试中让面试官眼前一亮,话不多说,直接上题目。

1、实现 call、apply、bind

1、实现 call

作用:使用一个指定的 this 值和一个或多个参数来调用一个函数;

  • 第一个参数为 null 或者 undefined 时,this 指向全局对象 window,值为原始值的指向该原始值的自动包装对象,如 String、Number、Boolean;
  • 为了避免函数名与上下文(context)的属性发生冲突,使用 Symbol 类型作为唯一值;
  • 将函数作为传入的上下文(context)属性执行;
  • 函数执行完成后删除该属性;
  • 返回执行结果;
// ES5
Function.prototype.call2 = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError('Type Error');
  }
  var context = context || window;
  // 将 this 绑定到对象上的一个方法
  context.fn = this;
  var args = [];
  // 储存函数变量参数
  for (var i=1; i<arguments.length; i++) {
    args.push('arguments[' + i + ']');
  }
  // 执行 context 上刚刚绑定的方法
  var result = eval('context.fn(' + args + ')');
  // 删除刚刚创建的方法
  delete context.fn;
  // 返回函数调用的返回值
  return result;
}

// ES6 (推荐)
// 传递参数从一个数组变成逐个传参了,不用...扩展运算符的也可以用 arguments 代替
Function.prototype.call2 = function (context, ...args) {
  // 这里默认不传就是给 window, 也可以用 ES6 给参数设置默认参数
  context = context || window;
  args = args ? args : [];
  // 给 context 新增一个独一无二的属性以免覆盖原有属性
  const key = Symbol();
  context[key] = this;
  // 通过隐式绑定的方式调用函数
  const result = context[key](...args);
  // 删除添加的属性
  delete context[key];
  // 返回函数调用的返回值
  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
  }
}
console.log(bar.call2(null)); // 2 
console.log(bar.call2(obj, 'kevin', 18)); // 1

2、实现 apply

作用:跟 call 一样,唯一的区别就是 call 是传入不固定个数的参数,而 apply 是传入一个数组;

  • 前部分与 call 一样;
  • 第二个参数可以不传,但类型必须为数组或者类数组;
// ES5
Function.prototype.apply2 = function(context,arr) {
  if (typeof this !== 'function') {
    throw new TypeError('Type Error');
  }
  var context = context || window;
  context.fn = this;
  if (!arr) {
   var result = context.fn();
  } else {
    var args = [];
    for (var i=0; i<arr.length; i++) {
      args.push('arr[' + i + ']');
    }
    var result = eval('context.fn(' + args + ')');
  }
  delete context.fn;
  return result;
}
// ES6 (推荐)
Function.prototype.apply2 = function (context, args) {
  // 这里默认不传就是给 window, 也可以用 ES6 给参数设置默认参数
  context = context || window;
  args = args ? args : [];
  // 给 context 新增一个独一无二的属性以免覆盖原有属性
  const key = Symbol();
  context[key] = this;
  // 通过隐式绑定的方式调用函数
  const result = context[key](...args);
  // 删除添加的属性
  delete context[key];
  // 返回函数调用的返回值
  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
  }
}
console.log(bar.apply2(null)); // 2
console.log(bar.apply2(obj, 'kevin', 18)); // 1

3、实现 bind

作用:bind 方法会创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用;

需要考虑:

  • bind() 除了 this 外,还可传入多个参数;
  • bind 创建的新函数可能传入多个参数;
  • 新函数可能被当做构造函数调用;
  • 函数可能有返回值;

实现方法:

  • bind 方法不会立即执行,需要返回一个待执行的函数;(闭包)
  • 实现作用域绑定(apply);
  • 参数传递(apply 的数组传参);
  • 当作为 构造函数 的时候,进行 原型继承;
// ES5
Function.prototype.bind = function (oThis) {
  var aArgs = Array.prototype.slice.call(arguments, 1);
  var fToBind = this;
  var fNOP = function () {};
  var fBound = function () {
    fBound.prototype = this instanceof fNOP ? new fNOP() : fBound.prototype;
    return fToBind.apply(this instanceof fNOP ? this : oThis || this, aArgs )
  }   
  if( this.prototype ) {
    fNOP.prototype = this.prototype;
  }
  return fBound;
}

// ES6 (推荐)
Function.prototype.bind2 = function (context, ...args) {
  const fn = this;
  args = args ? args : [];
  return function newFn(...newFnArgs) {
    // 判断是否作为构造函数使用的情况
    if (this instanceof newFn) {
      return new fn(...args, ...newFnArgs)
    }
    // 当作为普通函数执行,用 apply 执行函数
    return fn.apply(context, [...args,...newFnArgs])
  }
}

// 测试一下
var value = 2;
var obj = {
  value: 1
}
function bar(name, age) {
  console.log(this.value);
  return {
    value: this.value,
    name: name,
    age: age
  }
}
const b = bar.bind2(null, 'kevin'); // 2
b(18) // 2
const b_ = bar.bind2(obj, 'kevin');
b_(18); // 1

2、实现 new

作用:new 运算符用来创建用户自定义的对象类型的实例或者具有构造函数的内置对象的实例。

  • 创建一个新对象;
  • 设置原型,将对象的原型设置为函数的 prototype 对象;
  • 让函数的 this 指向这个对象,并执行构造函数的代码 (为这个新对象添加属性);
  • 判断函数的返回值类型,如果是值类型,返回创建的对象, 如果是引用类型,就返回这个引用类型的对象;
// ES5 (推荐)
function newFunc() {
  // 创建一个新对象
  var obj = new Object();
  // 将第一个参数提取出来,也就是构造函数
  var Constructor = [].shift.call(arguments);
  // 设置原型,将对象的原型设置为函数的 `prototype` 对象。
  obj.__proto__ = Constructor.prototype;
  // 让函数的 `this` 指向这个对象,并执行构造函数的代码(为这个新对象添加属性)
  var ret = Constructor.apply(obj, arguments);
  // 判断函数的返回值类型,如果是值类型,返回新创建的对象。如果是引用类型,就返回这个引用类型的对象。
  return typeof ret === 'object' ? ret : obj;
}

// ES6 (推荐)
function newFunc(...args) {
  const obj = Object.create({});
  const Constructor = [].shift.call(args);
  obj.__proto__ = Constructor.prototype;
  const result = Constructor.apply(obj, args);
  return typeof result === 'object' ? result : obj;
}

function F(name, age) {
  this.name = name;
  this.age = age
}
const newF = newFunc(F, 'dell', 18);
console.log(newF.name, newF.age); // dell 18

3、7 种继承方式

1、原型链继承

优点:

  • 能够继承父类的属性和方法; 缺点:
  • 问题1:原型中包含的 引用类型属性 将被所有实例共享(不能实现 多继承);
  • 问题2:子类实例化 的时候不能给父类构造函数传参;
  • 问题3:子类实例化后不能获取子类原型上的属性和方法;
function Parent() {
  this.name = 'parent1';
  this.play = [1, 2, 3];
  this.add = function() {
    return {
      name: this.name,
      play: [1,2,3]
    }
  }
}
Parent.prototype.getName = function () {
  return this.name;
}
function Child() {
  this.type = 'child2';
  this.add2 = function() {
    return this.type;
  }
}
Child.prototype.add1 = function() {
  return this.type;
}
Child.prototype.dell = 'dell';
// 原型链继承
Child.prototype = new Parent();
// 创建实例
var c = new Child();
console.log(c.name, c.type, c.add(), c.add2(), c.getName()); // 都可以获取到

// 无法读取本身原型上的属性和方法
console.log(c.add1()); // 不能读取到
console.log(c.dell);

// 不能使用多继承,可以修改父级原型上的属性,但如果这个属性是引用属性,那就会共享同一个实例,如下:
var child1 = new Child();
var child2 = new Child();
child1.name = 'dell';
child1.play[2] = 56;
console.log(child1.name); // dell
console.log(child2.name); // parent1
console.log(child1.play); // [1,2,56]
console.log(child2.play); // [1,2,56]

2、构造函数继承

优点:

  • 解决了原型链继承的问题 (1、不能实现多继承;2、子类实例化时不能向构造函数传参); 缺点:
  • 只能继承父类的属性和方法,不能继承父类原型上的属性和方法;
  • 由于方法是定义在构造函数中,所以每次子类实例化时都会创建一次方法;
function Parent(name) {
  this.name = name;
  this.age = '18';
  this.play = [1, 2, 3];
  this.getAge = function() {
    return this.age;
  }
}
Parent.prototype.lee = 'lee';
Parent.prototype.getName = function () {
  return this.name;
}
function Child(name) {
  // 传参
  Parent.call(this, name); // 继承父类
  this.type = 'child2';
  this.getDell_ = function() {
    return this.age;
  }
}
Child.prototype.getDell = function() {
  return this.age;
}
// 创建实例
var c1 = new Child('name');
var c2 = new Child('name');

// 可以获取子类原型属性和方法
console.log(c1.name, c1.age, c1.play, c1.getAge(), c1.getDell_(), c1.getDell()) // parent1 18 18 18 18

// 可以实现多继承 即不再共享同一个实例
c1.play[1] = 99
console.log(c1.play) // [1, 99, 3]
console.log(c2.play) // [1, 2, 3]

// 不能获取父类原型上的属性和方法
console.log(c1.lee) // undefined
console.log(c1.getName()) // 报错

3、组合继承

优点:

  • 解决了原型链继承或者构造函数继承单独使用产生的问题; 缺点:
  • 由于组合继承是结合了原型链继承和构造函数继承的方式,在原型链继承和构造函数继承都调用了一次父类,因此会使得父类被调用了 2 次;
function Parent() {
  console.log('两次调用')
  this.name = 'parent1';
  this.age = '18';
  this.play = [1, 2, 3];
  this.getAge = function() {
    return this.age
  }
}
Parent.prototype.lee = 'lee'
Parent.prototype.getName = function () {
  return this.name;
}
function Child() {
  Parent.call(this); // 第二次调用 Parent
  this.type = 'child2';
}
Child.prototype.getDell = function() {
  return this.age
}
Child.prototype = new Parent(); // 第一次调用 Parent
Child.prototype.constructor = Child; // 手动挂上 constructor 避免不能访问到 Parent
var c = new Child();
console.log(c.name, c.age, c.getAge()) // parent1 18 18
console.log(c.lee) // lee
console.log(c.getName()) // parent1

4. 原型式继承

优点:

  • 能够继承父类的属性和方法; 缺点:
  • 没有解决引用类型共享问题(不能实现多继承);
var Parent = {
  name: 'dell',
  arr: [18, 19, 20],
  getName: function() {
    return this.name
  }
}
let p1 = Object.create(Parent);
p1.name = 'tom'
p1.arr.push('lee')
let p2 = Object.create(Parent);
p2.name = 'lee';
p2.arr.push('dell')
console.log(p1.name, p1.arr, p1.getName())// tom [18, 19, 20, "lee", "dell"] tom
console.log(p2.name, p2.arr, p2.getName()) // lee [18, 19, 20, "lee", "dell"] lee
// 优点:能够继承属性和方法 缺点:没有解决引用问题,创建出来的引用时一样的

5、寄生式继承

优点:

  • 能够继承父类的属性和方法; 缺点:
  • 没有解决引用类型共享问题(不能实现多继承);
var Parent = {
  name: 'dell',
  arr: [18, 19, 20],
  getName: function() {
    return this.name
  }
}
let p1 = Object.create(Parent);
p1.getArr = function() {
  return this.name
}
console.log(p1.name, p1.arr, p1.getName(), p1.getArr()) // dell [18, 19, 20] dell dell

6、组合寄生式继承

优点:

  • 解决了组合继承方式出现的调用 2 次父类的问题(通过 Object.create(Parent.prototype) 创建一个副本,再副本上进行继承就不会出现调用两次父类的问题) 缺点:
  • 书写方式过于麻烦,推荐使用 ES6 class 继承方式;
function Parent() {
  console.log('执行一次,没有执行两次')
  this.name = 'dell'
  this.age = 18
  this.getName = function() {
    return this.name
  }
}
Parent.prototype.getAge = function() {
  return this.age;
}
function Child() {
  Parent.call(this);
  this.type = 'type'
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
// 创建实例
var p = new Child();
console.log(p.name, p.age, p.getName()) // dell 18 dell
console.log(p.getAge()) // 18
console.log(p.type) // type
// console.log(c.getDell()) // 为啥访问不到子类原型上的方法呢

7、ES6 继承方式

// class 相当于 ES5 中构造函数;
// class 中定义方法时,前后不能加 function,全部定义在 class 的 protopyte 属性中;
// class 中定义的所有方法是不可枚举的;
// class 中只能定义方法,不能定义对象、变量等;
// class 和方法内默认都是严格模式;
// ES5 中 constructor 为隐式属性;
class People {
  constructor(name='dai',age='27') {
    this.name = name;
    this.age = age;
  }
  eat() {
    console.log(`${this.name} ${this.age} eat food`);
  }
}
// 继承父类
class Woman extends People { 
   constructor(name = 'dai',age = '27') { 
     // 继承父类属性
     super(name, age); 
   } 
    eat() { 
     // 继承父类方法
      super.eat()
    } 
} 
let wonmanObj = new Woman('dai'); 
wonmanObj.eat();

// ES5 继承先创建子类的实例对象,然后再将父类的方法添加到 this 上(Parent.apply(this));
// ES6 继承是使用关键字 super 先创建父类的实例对象 this,最后在子类 class 中修改 this;

4、节流与防抖

1. 防抖 (debounce)

  • 高频事件触发后,n 秒后只执行一次,如果 n 秒内再次被触发,则重新计算时间;
  • 场景:input 搜索框搜索输入;
<input oninput="debounceInput(event)" />

function debounce(fn, delay) {
  var timeout = null;
  return function() {
    // 清除定时器,重新计算时间
    clearTimeout(timeout);
    // 延时,时间到后执行函数
    timeout = setTimeout(() => {
      fn.apply(this, arguments)
    }, delay)
  }
}

function onInput(event) {
  if (event.target.value) {
    console.log(event.target.value);
  }
}

let debounceInput = debounce(onInput, 300)
// 用途: 用于 input 框的搜索

2. 节流

  • 高频事件触发后,n 秒内只执行一次,节流是稀释函数的执行频率;
  • 场景:长列表滚动节流、resize; 2.1 时间戳
  • 首节流,第一次立刻执行,但是停止出发后,没办法再执行;
// 时间戳实现方式
function throttle(fn, delay) {
  var last = 0;
  return function () {
    var now = Date.now();
    if (now - last >= delay) {
      last = now;
      fn.apply(this, arguments)
    }
  }
}

2.2 setTimeout

  • 尾节流,不会立刻执行函数,而是在 delay 之后执行函数;
// 定时器实现
// 最后一次停止触发后,因为 delay 的定时器,还会最后执行一次;
function throttle(fn, delay) {
  var timer = null;
  return function() {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, arguments);
        timer = null;
      }, delay)
    }
  }
}

2.3 时间戳 + setTimeout

  • 实现了首节流和尾节流的方式
// 时间戳+定时器实现
function throttle(fn, delay){
  let timer = null;
  let startTime = 0;
  return function() {
    let curTime = Date.now();
    let remaining = delay - (curTime - startTime);
    clearTimeout(timer);
    if (remaining <= 0) {
      fn.aplly(this, arguments);
      startTime = Date.now();
    } else {
      timer = setTimeout(() => {
        fn.aplly(this, arguments);
        startTime = Date.now();
      }, remaining)
    }
  }
}

5、Promise 系列手写

1. 手写 Promise 基础版

class Prom {
  static resolve (value) {
    if (value && value.then) {
      return value
    }
    return new Prom(resolve => resolve(value))
  }

  constructor (fn) {
    this.value = undefined
    this.reason = undefined
    this.status = 'PENDING'

    // 维护一个 resolve/pending 的函数队列
    this.resolveFns = []
    this.rejectFns = []

    const resolve = (value) => {
      // 注意此处的 setTimeout
      setTimeout(() => {
        this.status = 'RESOLVED'
        this.value = value
        this.resolveFns.forEach(({ fn, resolve: res, reject: rej }) => res(fn(value)))
      })
    }

    const reject = (e) => {
      setTimeout(() => {
        this.status = 'REJECTED'
        this.reason = e
        this.rejectFns.forEach(({ fn, resolve: res, reject: rej }) => rej(fn(e)))
      })
    }

    fn(resolve, reject)
  }

  then (fn) {
    if (this.status === 'RESOLVED') {
      const result = fn(this.value)
      // 需要返回一个 Promise
      // 如果状态为 resolved,直接执行
      return Prom.resolve(result)
    }
    if (this.status === 'PENDING') {
      // 也是返回一个 Promise
      return new Prom((resolve, reject) => {
        // 推进队列中,resolved 后统一执行
        this.resolveFns.push({ fn, resolve, reject })
      })
    }
  }

  catch (fn) {
    if (this.status === 'REJECTED') {
      const result = fn(this.value)
      return Prom.resolve(result)
    }
    if (this.status === 'PENDING') {
      return new Prom((resolve, reject) => {
        this.rejectFns.push({ fn, resolve, reject })
      })
    }
  }
}

Prom.resolve(10).then(o => o * 10).then(o => o + 10).then(o => {
    console.log(o)
})

return new Prom((resolve, reject) => {
    reject('Error')
}).catch(e => {
    console.log('Error', e)
})

2. Promise.resolve

  • Promise.resolve(value) 可以将任何值转成值为 value 状态是 fulfilledPromise,但如果传入的值本身是 Promise 则会原样返回它;
Promise.resolve = (param) => {
  if(param instanceof Promise) return param;
  return new Promise((resolve, reject) => {
    if(param && param.then && typeof param.then === 'function') {
      // param 状态变为成功会调用 resolve,将新 Promise 的状态变为成功,反之亦然
      param.then(resolve, reject);
    } else {
      resolve(param);
    }
  })
}

3. Promise.reject

  • Promise.resolve() 类似,Promise.reject() 会实例化一个 rejected 状态的 Promise。但与 Promise.resolve() 不同的是,如果给 Promise.reject() 传递一个 Promise 对象,则这个对象会成为新 Promise 的值;
Promise.reject = function(reason) {
  return new Promise((resolve, reject) => reject(reason))
}

4. Promise.prototype.finally

  • 不管成功还是失败,都会走到 finally 中,并且 finally 之后,还可以继续 then。并且会将值原封不动的传递给后面的 then;
Promise.prototype.finally = function (callback) {
  return this.then((value) => {
    return Promise.resolve(callback()).then(() => {
      return value;
    });
  }, (err) => {
    return Promise.resolve(callback()).then(() => {
      throw err;
    });
  });
}

5. Promise.all

  • 传入的所有 Promsie 都是 fulfilled,则返回由他们的值组成的,状态为 fulfilled 的新 Promise
  • 只要有一个 Promiserejected,则返回 rejected 状态的新 Promsie,且它的值是第一个 rejectedPromise 的值;
  • 只要有一个 Promisepending,则返回一个 pending 状态的新 Promise
function PromiseAll(promiseArray) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promiseArray)) {
      return reject(new Error('传入的参数必须是数组!'));
    }
    let arr = [];
    const len = promiseArray.length;
    let counter = 0;
    for (let i=0; i<len; i++) {
      Promise.resolve(promiseArray[i]).then((res) => {
        // arr.push(res);
        counter++;
        arr[i] = res;
        if (counter === len) {
          resolve(arr);
        }
      }).catch(err => reject(err))
    }
  })
}
const pro1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('1')
  }, 1000)
})
const pro2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('2')
  }, 2000)
})
const pro3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('3')
  }, 3000)
})
const promise = PromiseAll([pro1, pro2, pro3]).then((res) => {
  console.log(res)
}).catch(err => console.log(err))

6. Promise.race

  • Promise.race 会返回一个由所有可迭代实例中第一个 fulfilledrejected 的实例包装后的新实例。
Promise.race = function(promiseArr) {
  return new Promise((resolve, reject) => {
    for (let i=0; i<promiseArr.length; i++) {
      Promise.resolve(promiseArr[i]).then(res => {
        resolve(res)
      }).catch(err => reject(err))
    }
  })
}

7. Promise.allSettled

  • Promise.allSettled() 方法返回一个 promise,该 promise 在所有给定的 promise 已被解析或被拒绝后解析,并且每个对象都描述每个 promise 的结果。
// 如下格式
Promise.allSettled([
  Promise.resolve('a'),
  Promise.reject('b'),
])
.then(arr => {
  console.log(res);
  // [
  //   { status: 'fulfilled', value:  'a' },
  //   { status: 'rejected',  reason: 'b' },
  // ]
});
function PromiseAllSettled(arr) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(arr)) {
      throw new Error('必须是一个数组')
    }
    let count = 0;
    let len = arr.length;
    let array = [];
    for (let i=0; i<len; i++) {
      Promise.resolve(arr[i]).then((item) => {
        count++;
        array[i] = {status: 'fulfilled', value: item};
        if (count >= len) {
          resolve(array)
        }
      }).catch((reason) => {
        count++;
        array[i] = {status: 'rejected', value: reason};
        if (count >= len) {
          resolve(array)
        }
      })
    }
  })
}

const pro1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('1')
  }, 1000)
})
const pro2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('2')
  }, 2000)
})
const pro3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('3')
  }, 3000)
})
const promise = PromiseAllSettled([pro1, pro2, pro3]).then((res) => {
  console.log(res)
}).catch(err => console.log(err))

8. Promise.any

  • 该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。只要参数实例有一个变成 fulfilled 状态,包装实例就会变成 fulfilled 状态;如果所有参数实例都变成 rejected 状态,包装实例就会变成 rejected 状态。

  • Promise.any()Promise.race() 方法很像,只有一点不同,就是 Promise.any() 不会因为某个 Promise 变成 rejected 状态而结束,必须等到所有参数 Promise 变成 rejected 状态才会结束。

// Promise.any
function PromiseAny(arr) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(arr)) {
      throw new Error('必须传入一个数组')
    }
    let len = arr.length;
    let count = 0;
    let result = [];
    for (let i=0; i<len; i++) {
      Promise.resolve(arr[i]).then(res => {
        resolve(res);
      }).catch(err => {
        result[i] = err;
        count++;
        if (count === len) {
          reject(`AggregateError: All promises were rejected`)
        }
      })
    }
  })
}
// 测试
const pro1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('1')
  }, 1000)
})
const pro2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('2')
  }, 2000)
})
const pro3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('3')
  }, 3000)
})
const promise = PromiseAny([pro1, pro2, pro3]).then((res) => {
  console.log(res)
}).catch(err => console.log(err))

6、Ajax的实现

  • 使用 Promise 封装 Ajax;
function ajax(method, url, body, headers) {
  // 返回一个 Promise
  return new Promise((resolve, reject) => {
    // 初始化一个 XMLHttpRequest()对象
    let xhr = new XMLHttpRequest();
    // 调用 open 方法,并传入三个参数:请求方法,url,同步异步
    xhr.open(methods, url, true);
    // 循环判断请求头,并设置请求头 setRequestHeader()
    for(let key in headers) {
      if (headers.hasOwnProperty(key)) {
        xhr.setRequestHeader(key, headers[key])
      }
    }
    // 监听 onreadystatechange() 事件,如果 readystate === 4 返回responseText
    xhr.onreadystatechange(() => {
      if(xhr.readyState == 4) {
        if(xhr.status >= '200' && xhr.status <= 300){
          resolve(xhr.responeText)
        } else {
          reject(xhr)
        }
      }
    })
    // 调用 send() 方法 传递参数
    xhr.send(body)
  })
}

7、async / await

function asyncToGenerator(generatorFunc) {
  // 返回的是一个新的函数
  return function() {
  
    // 先调用generator函数 生成迭代器
    // 对应 var gen = testG()
    const gen = generatorFunc.apply(this, arguments)

    // 返回一个promise 因为外部是用.then的方式 或者await的方式去使用这个函数的返回值的
    // var test = asyncToGenerator(testG)
    // test().then(res => console.log(res))
    return new Promise((resolve, reject) => {
    
      // 内部定义一个step函数 用来一步一步的跨过yield的阻碍
      // key有next和throw两种取值,分别对应了gen的next和throw方法
      // arg参数则是用来把promise resolve出来的值交给下一个yield
      function step(key, arg) {
        let generatorResult
        
        // 这个方法需要包裹在try catch中
        // 如果报错了 就把promise给reject掉 外部通过.catch可以获取到错误
        try {
          generatorResult = gen[key](arg)
        } catch (error) {
          return reject(error)
        }

        // gen.next() 得到的结果是一个 { value, done } 的结构
        const { value, done } = generatorResult

        if (done) {
          // 如果已经完成了 就直接resolve这个promise
          // 这个done是在最后一次调用next后才会为true
          // 以本文的例子来说 此时的结果是 { done: true, value: 'success' }
          // 这个value也就是generator函数最后的返回值
          return resolve(value)
        } else {
          // 除了最后结束的时候外,每次调用gen.next()
          // 其实是返回 { value: Promise, done: false } 的结构,
          // 这里要注意的是Promise.resolve可以接受一个promise为参数
          // 并且这个promise参数被resolve的时候,这个then才会被调用
          return Promise.resolve(
            // 这个value对应的是yield后面的promise
            value
          ).then(
            // value这个promise被resove的时候,就会执行next
            // 并且只要done不是true的时候 就会递归的往下解开promise
            // 对应gen.next().value.then(value => {
            //    gen.next(value).value.then(value2 => {
            //       gen.next() 
            //
            //      // 此时done为true了 整个promise被resolve了 
            //      // 最外部的test().then(res => console.log(res))的then就开始执行了
            //    })
            // })
            function onResolve(val) {
              step("next", val);
            },
            // 如果promise被reject了 就再次进入step函数
            // 不同的是,这次的try catch中调用的是gen.throw(err)
            // 那么自然就被catch到 然后把promise给reject掉啦
            function onReject(err) {
              step("throw", err);
            },
          )
        }
      }
      step("next");
    })
  }
}

8、数组扁平化

1. 普通递归

var arr = [1,2,3,4,[4,5,[5,6,7]]]
function flatten(arr) {
  let array = [];
  for (let i=0; i<arr.length; i++) {
    if (Array.isArray(arr[i])) {
      array = array.concat(flatten(arr[i]));
    } else {
      array.push(arr[i]);
    }
  }
  return array;
}
const result = flatten(arr);
console.log(result);

2. reduce

var arr = [1,2,3,4,[4,5,[5,6,7]]];
function flatten(arr) {
  return arr.reduce((prev, cur) => {
    return prev.concat(Array.isArray(cur) ? flatten(cur) : cur);
  }, [])
}
const result = flatten(arr);
console.log(result);

3.toString + split

var arr = [1,2,3,4,[4,5,[5,6,7]]];
function flatten(arr) {
  return arr.toString().split(',').map((res)=>{
    return Number(res);
  })
}
const result = flatten(arr);
console.log(result);

4. 调用 ES6 中的 flat

var arr = [1,2,3,4,[4,5,[5,6,7]]];
function flatten(arr) {
    return arr.flat(Infinity);
}
const result = flatten(arr);
console.log(result);

5. 正则和 JSON 方法共同处理

let arr = [1, [2, [3, [4, 5]]], 6];
function flatten(arr) {
  let str = JSON.stringify(arr);
  str = str.replace(/(\[|\])/g, '');
  str = '[' + str + ']';
  return JSON.parse(str); 
}
console.log(flatten(arr)); //  [1, 2, 3, 4,5]

6. 带第二个参数扁平化--记住

// 数组扁平化
const arr = [1, 2, 3, 4, [4, [5, 6, [7, 8, 9]]]];
function flatten(arr, index) {
  return index > 0 ? arr.reduce((prev, curr) => {
    return prev.concat(Array.isArray(curr) ? flatten(curr, index - 1) : curr);
  }, []) : arr.slice();
}
console.log(arr.flat(2));
console.log(flatten(arr, 2));

7. 变形

// 数组扁平化
const arr = [1, 2, 3, 4, [4, [5, 6, [7, 8, 9]]]];
function flatten(arr, index) {
  let array = [];
  if (index > 0) {
    for (let i=0; i<arr.length; i++) {
      if (Array.isArray(arr[i])) {
        array = array.concat(flatten(arr[i], index - 1))
      } else {
        array.push(arr[i])
      }
    }
  } else {
    array = arr.slice();
  }
  return array;
}
console.log(arr.flat(3));
console.log(flatten(arr, 3));

8. 变形2--forEach

// 数组扁平化
const arr = [1, 2, 3, 4, [4, [5, 6, [7, 8, 9]]]];
function flatten(arr, index) {
  let array = [];
  if (index > 0) {
    arr.forEach(item => {
      if (Array.isArray(item)) {
        array = array.concat(flatten(item, index - 1))
      } else {
        array.push(item);
      }
    })
  } else {
    array = arr.slice();
  }
  return array;
}
console.log(arr.flat(2));
console.log(flatten(arr, 2));

9、instanceof 函数

  • instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 AB 的实例,则返回 true,否则返回 false;
  • instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性;
  • 不能检测基本数据类型,在原型链上的结果未必准确,不能检测 null,undefined;
  • 实现:遍历左边变量的原型链,直到找到右边变量的 prototype,如果没有找到,返回 false;
function instanceOf(left, right) {
  // 获得对象的原型
  var proto = left.__proto__;
  // 获得类型的原型
  var prototype = right.prototype;
  while(true) {
    if (proto === null) return false;
    if (proto === prototype) return true;
    proto = proto.__proto__;
  }
}
function a() {
  this.name = 'dell';
  this.age = 18;
}
var newA = new a();
console.log(instanceOf(newA, a))

10、数组的各种排序

1. 冒泡排序

var a = [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221];
function bubbleSort(array) {
  if (array.length < 2) return array;
  for (let i=0; i<array.length; i++) {
    for (let j=i+1; j<array.length; j++) {
      if (array[j]<array[i]) {
        let num = array[i];
        array[i] = array[j];
        array[j] = num;
      }
    }
  }
  return array;
}
let arr = bubbleSort(a);
console.log(arr);

2. 快速排序

var a = [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221];
function quickSort(array) {
  if (array.length <= 1) return array;
  const len = array.length;
  const index = Math.floor(len >> 1);
  const pivot = array.splice(index, 1)[0]; // 使用 splice 会返回删除的形成数组
  const left = [];
  const right = [];
  for (let i=0; i<len; i++) {
    if (array[i] <= pivot) {
      left.push(array[i]);
    } else if (array[i] > pivot) { // 坑啊  一定要写 else if {}, 不能写 else {}
      right.push(array[i]);
    }
  }
  return quickSort(left).concat([pivot], quickSort(right));
}
const arr = quickSort(a);
console.log(arr);

3. 插入排序

var a = [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221];
function insertSort(array) {
  const len = array.length;
  let current;
  let prev;
  for (let i=1; i<len; i++) {
    current = array[i];
    prev = i - 1;
    while (prev >= 0 && array[prev] > current) {
      array[prev + 1] = array[prev];
      prev--;
    }
    array[prev + 1] = current;
  }
  return array
}
const arr = insertSort(a);
console.log(arr);

4. 选择排序

var a = [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221];
function selectSort(array) {
  const len = array.length
  let temp
  let minIndex
  for (let i = 0; i < len - 1; i++) {
    minIndex = i;
    for (let j = i + 1; j < len; j++) {
      if (array[j] <= array[minIndex]) {
        minIndex = j;
      }
    }
    temp = array[i];
    array[i] = array[minIndex];
    array[minIndex] = temp;
  }
  return array;
}
selectSort(a); // [1, 1, 3, 3, 6, 6, 23, 34, 76, 221, 222, 456]

5. 堆排序

var a = [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221];
function heap_sort(arr) {
  var len = arr.length;
  var k = 0;
  function swap(i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
  }
  function max_heapify(start, end) {
    var dad = start;
    var son = dad * 2 + 1;
    if (son >= end) return;
    if (son + 1 < end && arr[son] < arr[son + 1]) {
      son++;
    }
    if (arr[dad] <= arr[son]) {
      swap(dad, son);
      max_heapify(son, end);
    }
  }
  for (var i = Math.floor(len / 2) - 1; i >= 0; i--) {
    max_heapify(i, len)
  }  
  for (var j = len - 1; j > k; j--) {
    swap(0, j);
    max_heapify(0, j);
  }  
  return arr;
}
heap_sort(a); // [1, 1, 3, 3, 6, 6, 23, 34, 76, 221, 222, 456]

6. 归并排序

var a = [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221];
function mergeSort(array) {
  const merge = (right, left) => {
    const result = [];
    let il = 0;
    let ir = 0;
    while (il < left.length && ir < right.length) {
      if (left[il] < right[ir]) {
        result.push(left[il++]);
      } else {
        result.push(right[ir++]);
      }
    }
    while (il < left.length) {
      result.push(left[il++]);
    }
    while (ir < right.length) {
      result.push(right[ir++]);
    }
    return result;
  }
  const mergeSort = array => {
    if (array.length === 1) { return array }
    const mid = Math.floor(array.length / 2);
    const left = array.slice(0, mid);
    const right = array.slice(mid, array.length);
    return merge(mergeSort(left), mergeSort(right));
  }
  return mergeSort(array);
}
mergeSort(a); // [1, 1, 3, 3, 6, 6, 23, 34, 76, 221, 222, 456]

11、柯里化函数

// 递归遍历
function curry(fn, ...args) {
  var len = fn.length;
  var args = args || [];
  return function() {
    var newArgs = args.concat(Array.prototype.slice.call(arguments));
    if (newArgs.length < len) {
      return curry.call(this, fn, ...newArgs)
    } else {
      return fn.apply(this, newArgs);
    }
  }
}

function add(a, b, c) {
  return a + b + c;
}
var a = curry(add);
console.log(a(1, 2, 3));
// ES6
function curry(fn, ...args) {
  if (args.length < fn.length) return (...args1) => curry(fn, ...args, ...args1);
  else return fn(...args);
}

function add(a, b, c) {
  return a + b + c;
}
var a = curry(add);
var b = a(1,2,3);
console.log(b);

闭包 JS基础 编程题 (字节)

var foo = function(...args) {
 // 要求实现函数体
}
var f1 = foo(1,2,3);
f1.getValue(); // 6 输出是参数的和
var f2 = foo(1)(2,3);
f2.getValue(); // 6
var f3 = foo(1)(2)(3)(4);
f3.getValue(); // 10

解答

var foo = function(...args) {
  const target = (...args2) => foo(...args, ...args2);
  target.getValue = () => args.reduce((p, q) => p + q, 0)
  return target
}
var f1 = foo(1,2,3);
console.log(f1.getValue()) // 6 输出是参数的和
var f2 = foo(1)(2, 3);
console.log(f2.getValue()); // 6
var f3 = foo(1)(2)(3)(4);
console.log(f3.getValue()) // 10

柯里化变形编程题

完成 combo 函数。它接受任意多个单参函数(只接受一个参数的函数)作为参数,并且返回一个函数。它的作为用:使得类似 f(g(h(a))) 这样的函数调用可以简写为 combo(f, g, h)(a)

// ES6 -- reduce
const combo = (...args) => {
  args.length && args.reverse()
  return prop =>
    args.reduce((acc, cur) => {
      return cur(acc);
    }, prop)
}
// ES5 -- for
function combo () {
  const cbs = [...arguments].reverse();
  return function () {
    var res = cbs[0](...arguments);
    for (let i = 1; i < cbs.length; i++) {
      res = cbs[i](res);
    }
    return res;
  }
}

/* 以下为测试代码 */
const addOne = (a) => a + 1
const multiTwo = (a) => a * 2
const divThree = (a) => a / 3
const toString = (a) => a + ''
const split = (a) => a.split('')

split(toString(addOne(multiTwo(divThree(666)))))
// => ["4", "4", "5"]

const testForCombo = combo(split, toString, addOne, multiTwo, divThree)
testForCombo(666)
// => ["4", "4", "5"]

12、数组去重

1. Set

let arr = [1,2,3,4,4,5,6,7];
[...new Set(arr)]

2. Set + Array.from

let arr = [1,2,3,4,4,5,6,7];
function unique(arr) {
  return Array.from(new Set(arr));
}

3. reduce + includes

let arr = [1,2,3,4,4,5,6,7];
function unique(arr) {
  return arr.reduce((prev, cur) => prev.includes(cur) ? prev : [...prev, cur], []);
}
const uniqueArr = unique(arr);
console.log(uniqueArr);

4. filter + indexOf

let arr = [1,2,3,4,4,5,6,7];
function unique(arr) {
  return arr.filter((item, index, arr) => {
    return arr.indexOf(item) === index;
  })
}
const uniqueArr = unique(arr);
console.log(uniqueArr);

5. for + indexOf

let arr = [1,2,3,4,4,5,6,7];
function unique(arr) {
  let array = []
  for (let i=0; i<arr.length; i++) {
    if (arr.indexOf(arr[i]) === i) {
      array.push(arr[i]);
    }
  }
  return array;
}
const uniqueArr = unique(arr);
console.log(uniqueArr)

6. for + indexOf

let arr = [1,2,3,4,4,5,6,7];
function unique(arr) {
  let array = [];
  for (let i=0; i<arr.length; i++) {
    if (array.indexOf(arr[i]) === -1) {
      array.push(arr[i]);
    }
  }
  return array;
}
const uniqueArr = unique(arr);
console.log(uniqueArr);

7. for + includes

let arr = [1,2,3,4,4,5,6,7];
function unique(arr) {
  let array = [];
  for (let i=0; i<arr.length; i++) {
    if (!array.includes(arr[i])) {
      array.push(arr[i])
    }
  }
  return array
}
const uniqueArr = unique(arr);
console.log(uniqueArr);

13、发布订阅模式

// 我们以 ES6 类的形式写出来
class EventEmitter {
  constructor() {
    // 事件对象,存储订阅的type类型
    this.events = Object.create(null);
  }
  /**
   * 注册事件监听者
   * @param {String} type 事件类型
   * @param {Function} cb 回调函数
   */
  on(type, cb) {
    // 如果该type类型存在,就继续向数组中添加回调cb
    if (this.events[type]) {
      this.events[type].push(cb);
    } else {
      // type类型第一次存入的话,就创建一个数组空间并存入回调cb
      this.events[type] = [cb];
    }
  }
  /**
   * 发布事件
   * @param {String} type 事件类型
   * @param  {...any} args 参数列表,把emit传递的参数赋给回调函数
   */
  emit(type, ...args) {
    // 遍历对应type订阅的数组,全部执行
    if (this.events[type]) {
      this.events[type].forEach(listener => {
        listener.call(this, ...args);
      });   
    }
  }
  /**
   * 移除某个事件的一个(相同)监听者
   * @param {String} type 事件类型
   * @param {Function} cb 回调函数
   */
  off(type, cb) {
    if (this.events[type]) {
      this.events[type] = this.events[type].filter(listener => {
        // 过滤用不到的回调cb
        return listener !== cb && listener.listen !== cb;
      });
    }
    if (this.events[type].length === 0) {
      delete this.events[type];
    }
  }
  /**
   * 移除某个事件的所有监听者
   * @param {String} type 事件类型
   */
  offAll(type) {
    if (this.events[type]) {
      delete this.events[type];
    }
  }
  /**
   * 只触发一次的发布
   * @param {String} type 事件类型
   * @param {Function} cb 回调函数
   */
  once(type, cb) {
    function wrap() {
      cb(...arguments);
      this.off(type, wrap);
    }
    // 先绑定,调用后删除
    wrap.listen = cb;
    // 直接调用on方法
    this.on(type, wrap);
  }
}
// 测试
// 创建事件管理器实例
const ee = new EventEmitter();
// 注册一个chifan事件监听者
ee.on('chifan', function() { console.log('吃饭了,我们走!') })
// 发布事件chifan
ee.emit('chifan');
// 也可以emit传递参数
ee.on('chifan', function(address, food) { console.log(`吃饭了,我们去${address}${food}!`) });
ee.emit('chifan', '三食堂', '铁板饭'); // 此时会打印两条信息,因为前面注册了两个chifan事件的监听者

// 测试移除事件监听
const toBeRemovedListener = function() { console.log('我是一个可以被移除的监听者') };
ee.on('testoff', toBeRemovedListener);
ee.emit('testoff');
ee.off('testoff', toBeRemovedListener);
ee.emit('testoff'); // 此时事件监听已经被移除,不会再有console.log打印出来了

// 测试移除chifan的所有事件监听
ee.offAll('chifan');
console.log(ee); // 此时可以看到ee.listeners已经变成空对象了,再emit发送chifan事件也不会有反应了

14、深浅拷贝

1. 浅拷贝

var obj = {a: 1, b: 2, c: { d: 1, e: 5 }}
function shallowClone(target) {
  if (typeof target === 'object' && target !== null) {
    var obj = Array.isArray(target) ? [] : {};
    for (var key in target) {
      if (target.hasOwnProperty(key)) {
        obj[key] = target[key]
      }
    }
    return obj;
  }

// 首层深拷贝 后面都是浅拷贝
var o = shallowClone(obj);
obj.c.d = 2;
console.log(obj); // {a: 1, b: 2, c: {d:2, e:5}}
console.log(o); // {a: 1, b: 2, c: {d:2, e:5}}

2. 深拷贝

  • 判断类型,正则和日期直接返回新对象
  • 空或者非对象类型,直接返回原值
  • 考虑循环引用,判断如果 hash 中含有直接返回 hash 中的值
  • 新建一个相应的 new obj.constructor 加入 hash
  • 遍历对象递归(普通 keykeysymbol 情况)
// 基础版
var obj = {a: 1, b: 2, c: { d: 1, e: 5 }}
function deepClone(target) {
  if (typeof target !== 'object' && target !== null) {
    throw new Error('请输入一个对象/数组');
  }
  let obj = Array.isArray(target) ? [] : {};
  for (const key in target) {
    if (target.hasOwnProperty(key)) {
      if (typeof target[key] === 'object') {
        obj[key] = deepClone(target[key]);
      } else {
        obj[key] = target[key];
      }
    }
  }
  return obj;
}
var o = deepClone(obj);
obj.c.d = 2;
console.log(obj); // {a: 1, b: 2, c: {d: 2, e: 5}}
console.log(o); // {a: 1, b: 2, c: {d: 1, e: 5}}
// 进阶版
function deepClone(obj,hash = new WeakMap()) {
  if(obj instanceof RegExp) return new RegExp(obj);
  if(obj instanceof Date) return new Date(obj);
  if(obj === null || typeof obj !== 'object') return obj;
  // 循环引用的情况;
  if(hash.has(obj)) {
    return hash.get(obj)
  }
  // new 一个相应的对象;
  // obj 为 Array,相当于 new Array();
  // obj 为 Object,相当于 new Object();
  let constr = new obj.constructor();
  hash.set(obj,constr);
  for(let key in obj) {
    if(obj.hasOwnProperty(key)) {
      constr[key] = deepClone(obj[key],hash)
    }
  }
  // 考虑 symbol 的情况;
  let symbolObj = Object.getOwnPropertySymbols(obj);
  for(let i=0;i<symbolObj.length;i++){
    if(obj.hasOwnProperty(symbolObj[i])) {
      constr[symbolObj[i]] = deepClone(obj[symbolObj[i]],hash)
    }
  }
  return constr
}

15、实现 sleep 方法

1. 回调

function sleep(callback, time) {
  setTimeout(callback, time);
}
function sayHi() {
  console.log('satHi');
}
sleep(sayHi, 1000);

2. promise

// 简单版
function sleep(time) {
  return new Promise((resolve, reject) => setTimeout(resolve, time));
}
sleep(1000).then(() => { console.log('sayHi') });
// 进阶版
function sleep(fn, time, ...args) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // reslove(fn(...args))
      Promise.resolve(fn(...args)).then(resolve).catch(reject);
    }, time)
  })
}

3. Generator

function* sleep(time) {
  yield new Promise(function(resolve, reject) {
    setTimeout(resolve, time);
  })
}
sleep(1000).next().value.then(()=>{console.log('sayHi')});

4. async / await

function sleep(time) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, time);
  })
}
aysnc function output(time) {
  const result = await sleep(time);
  console.log('sayHi');
  return result;
}
output(1000)

16、实现 Object.create()

  • MDN 文档;
  • Object.create() 会将参数对象作为一个新创建的空对象的原型, 并返回这个空对象;
// 简单版
function myCreate(obj) {
  // 新声明一个函数
  function C() {};
  // 将函数的原型指向obj
  C.prototype = obj;
  // 返回这个函数的实力化对象
  return new C();
}
// 官方版 Polyfill
if (typeof Object.create !== "function") {
  Object.create = function (proto, propertiesObject) {
    if (typeof proto !== 'object' && typeof proto !== 'function') {
      throw new TypeError('Object prototype may only be an Object: ' + proto);
    } else if (proto === null) {
      throw new Error("This browser's implementation of Object.create is a shim and doesn't support 'null' as the first argument.");
    }

    if (typeof propertiesObject !== 'undefined') throw new Error("This browser's implementation of Object.create is a shim and doesn't support a second argument.");

    function F() {}
    F.prototype = proto;

    return new F();
  };
}

17、实现 Object.assign

Object.assign2 = function(target, ...source) {
  if (target === null || target === undefined) {
    throw new TypeError('Cannot convert undefined or null to object')
  }
  let ret = Object(target);
  source.forEach(function(obj) {
    if (obj !== null || obj !== undefined) {
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
          ret[key] = obj[key];
        }
      }
    }
  })
  return ret;
}

18、实现每隔一秒打印(闭包)

for (var i=1; i<=5; i++) {
  (function (i) {
    setTimeout(() => console.log(i), 1000*i);
  })(i)
}
for (var i=1; i<=5; i++) {
  setTimeout(function(i) {
    console.log(i);
  }.bind(null, i), 1000*i)
}

19、手写一个 jsonp

let jsonp = function (url, data = {}, callback='callback') {
  // 转化数据为 url 字符串形式
  let dataStr = url.indexOf('?') === -1 ? '?' : '&'
  for(let key in data) {
    dataStr += `${key}=${data[key]}&`;
  }
  // 处理url中的回调函数
  dataStr += 'callback=' + callback;
  // 创建 srcipt 标签并添加 src 属性值
  let scriptBody = document.createElement('script')
  scriptBody.src = url + dataStr

  // append 到页面中添加到页面就立刻发起请求
  document.body.appendChild(scriptBody);
  // 返回一个 promise
  return new Promise((resolve, reject) => {
    window[callback] = (data) => {
      try {
        resolve(data)
      } catch(e) {
        reject(e)
      } finally {
        // 移除 script 元素
        scriptBody.parentNode.removeChild(scriptBody);
        console.log(scriptBody);
      }
    }
  })
}

jsonp('https://photo.sina.cn/aj/index', {page:1,cate:'recommend'}).then(res=>{console.log(res)})

20、手写观察者模式

在观察者模式中,只有两个主体,分别是目标对象 Subject,观察者 Observer

  • 观察者需 Observer 要实现 update 方法,供目标对象调用。update 方法中可以执行自定义的业务代码。
  • 目标对象 Subject 也通常被叫做被观察者或主题,它的职能很单一,可以理解为,它只管理一种事件。Subject 需要维护自身的观察者数组 observerList,当自身发生变化时,通过调用自身的 notify 方法,依次通知每一个观察者执行 update 方法。
// 观察者
class Observer {
  /**
   * 构造器
   * @param {Function} cb 回调函数,收到目标对象通知时执行
   */
  constructor(cb){
    if (typeof cb === 'function') {
      this.cb = cb;
    } else {
      throw new Error('Observer 构造器必须传入函数类型!');
    }
  }
  /**
   * 被目标对象通知时执行
   */
  update() {
    this.cb();
  }
}

// 目标对象
class Subject {
  constructor() {
    // 维护观察者列表
    this.observerList = [];
  }
  /**
   * 添加一个观察者
   * @param {Observer} observer Observer实例
   */
  addObserver(observer) {
    this.observerList.push(observer);
  }
  /**
   * 通知所有的观察者
   */
  notify() {
    this.observerList.forEach(observer => {
      observer.update();
    })
  }
}

const observerCallback = function() {
  console.log('我被通知了');
}
const observer = new Observer(observerCallback);

const subject = new Subject();
subject.addObserver(observer);
subject.notify();

21、获取对象内容

完成 deepGet 函数,给它传入一个对象和字符串,字符串表示对象深层属性的获取路径,可以深层次获取对象内容:

const deepGet = (obj, prop) => {
  const keyArr = prop.split('.').map(key => key);

  const reducer = (acc, cur) => {
    const objKey = cur.includes('[') && cur.replaceAll(/[\[?0-9\]$]/gi, '');
    if (Array.isArray(acc[objKey])) {
      cur = cur.replaceAll(/[^?0-9]/gi, '');
      return acc[objKey][cur] || {};
    }
    return acc[cur] ? acc[cur] : {};
  }

  const result = keyArr.reduce(reducer, obj);
  return Object.keys(result).length ? result : undefined;
}

/** 以下为测试代码 */
deepGet({
  school: {
    student: { name: 'Tomy' },
  },
}, 'school.student.name') // => 'Tomy'

deepGet({
  school: {
    students: [
      { name: 'Tomy' },
      { name: 'Lucy' },
    ],
  }
}, 'school.students[1].name') // => 'Lucy'

// 对于不存在的属性,返回 undefined
deepGet({ user: { name: 'Tomy' } }, 'user.age'); // => undefined
deepGet({ user: { name: 'Tomy' } }, 'school.user.age'); // => undefined

22、生成随机数的各种方法?

// 生成 [min, max) 的随机数, 其中 Math.random() 是生成 [0, 1) 的随机数
function getRandom(min, max) {
  return Math.floor(Math.random() * (max - min)) + min   
}
// 生成[min, max] 的随机数
function getRandom(min, max) {
  return Math.floor(Math.random() * (max + 1 - min)) + min
}
// 生成 (min, max) 的随机数
function getRandom(min, max) {
  let result = Math.random()*(m-n)+n;
  while(result == n) {
    result = Math.random()*(m-n)+n;
  }
  return result;
}
// 生成 (min, max] 的随机数
function getRandom(min, max) {
  let result = Math.random()*(m-n+1)+n-1;
  while(result<n) {
    result = Math.random()*(m-n+1)+n-1;
  }
  return result;
}

23、如何实现数组的随机排序?

let arr = [2,3,454,34,324,32];
arr.sort(randomSort);
function randomSort(a, b) {
  return Math.random() > 0.5 ? -1 : 1;
}

24、实现正则切分千分位

// 无小数点
let num1 = '1321434322222'
num1.replace(/(\d)(?=(\d{3})+$)/g,'$1,');
// 有小数点
let num2 = '342243242322.3432423';
num2.replace(/(\d)(?=(\d{3})+\.)/g,'$1,');

25、解析 URL Params 为对象

let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
parseParam(url);
/* 结果
{ 
  user: 'anonymous',
  id: [ 123, 456 ], // 重复出现的 key 要组装成数组,能被转成数字的就转成数字类型
  city: '北京', // 中文需解码
  enabled: true, // 未指定值得 key 约定为 true
}
*/
function parseParam(url) {
  const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来
  const paramsArr = paramsStr.split('&'); // 将字符串以 & 分割后存到数组中
  let paramsObj = {};
  // 将 params 存到对象中
  paramsArr.forEach(param => {
    if (/=/.test(param)) { // 处理有 value 的参数
      let [key, val] = param.split('='); // 分割 key 和 value
      val = decodeURIComponent(val); // 解码
      val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字

      if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则添加一个值
        paramsObj[key] = [].concat(paramsObj[key], val);
      } else { // 如果对象没有这个 key,创建 key 并设置值
        paramsObj[key] = val;
      }
    } else { // 处理没有 value 的参数
      paramsObj[param] = true;
    }
  })

  return paramsObj;
}

26、模板引擎实现

let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {
  name: '姓名',
  age: 18
}
render(template, data); // 我是姓名,年龄18,性别undefined
// 方法一
function render(template, data) {
  const reg = /\{\{(\w+)\}\}/; // 模板字符串正则
  if (reg.test(template)) { // 判断模板里是否有模板字符串
    const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段
    template = template.replace(reg, data[name]); // 将第一个模板字符串渲染
    return render(template, data); // 递归的渲染并返回渲染后的结构
  }
  return template; // 如果模板没有模板字符串直接返回
}
// 方法二(推荐)
function render(template, data) {
  let computed = template.replace(/\{\{(\w+)\}\}/g, function (match, key) {
    return data[key];
  });
  return computed;
}

27、转化为驼峰命名

var s1 = "get-element-by-id";
// 转化为 getElementById
var f = function(s) {
  return s.replace(/-\w/g, function(x) {
    return x.slice(1).toUpperCase();
  })
}

28、查找字符串中出现最多的字符和个数

  • 例: abbcccddddd -> 字符最多的是d,出现了5次
let str = "abcabcabcbbccccc";
let num = 0;
let char = '';

 // 使其按照一定的次序排列
str = str.split('').sort().join('');
// "aaabbbbbcccccccc"

// 定义正则表达式
let re = /(\w)\1+/g;
str.replace(re,($0,$1) => {
  if(num < $0.length) {
    num = $0.length;
    char = $1;        
  }
});
console.log(`字符最多的是${char},出现了${num}次`);

29、图片懒加载

let imgList = [...document.querySelectorAll('img')]
let length = imgList.length

const imgLazyLoad = function() {
  let count = 0
  return (function() {
    let deleteIndexList = []
    imgList.forEach((img, index) => {
      let rect = img.getBoundingClientRect()
      if (rect.top < window.innerHeight) {
        img.src = img.dataset.src;
        deleteIndexList.push(index);
        count++;
        if (count === length) {
          document.removeEventListener('scroll', imgLazyLoad);
        }
      }
    })
    imgList = imgList.filter((img, index) => !deleteIndexList.includes(index));
  })()
}

// 这里最好加上防抖处理
document.addEventListener('scroll', imgLazyLoad);

30、实现 reduce、forEach

// reduce
Array.prototype.reduce = function(fn, val) {
  // 很好理解,就判断 val 是否有传入值
  for (let i = 0; i < this.length; i++) {
    // 没有传入值
    if (typeof val === 'undefined') {
      val = this[i];
    } else { // 有传入 val 值
      // total就是初始值 val,之后的依次传入对应
      val = fn(val, this[i], i, this);
    }
  }
  return val;
};
// forEach
// 基础版本
Array.prototype.forEach = function(callback) {
  var len = this.length;
  for(var i = 0; i < len; i++) {
    callback(this[i], i, this);
  }
}
// call 实现
Array.prototype.forEach = function(callback, thisArg) {
  var len = this.length;
  for(var i = 0; i < len; i++) {
   // callback(this[i], i, this);
   callback.call(thisArg, this[i], i, this);
  }
}
// bind 实现
Array.prototype.forEach = function(callback, thisArg) {
  var len = this.length;
  callback = callback.bind(thisArg);
  for(var i = 0; i < len; i++) {
    callback(this[i], i, this);
  }
}

31、用 setTimeout 实现 setInterval

let timer = null;
function mockSetInterval(fn, delay, ...args) {
  let recur = function() {
    timer = setTimeout(() => {
      fn.apply(this, args);
      recur();
    }, delay)
  }
  recur();
}

function mockClearInterval(id) {
  clearTimeout(id);
}

mockSetInterval((a, b) => {console.log(a, b)}, 1000, '1', '2')

setTimeout(() => {
  mockClearInterval(timer);
}, 4000)

32、实现 chunk 函数

  • 根据 size 值,将数组划分为几段,例如,arr = [1,2,3,4,5,6,7], size = 2;
  • 返回:target = [[1,2], [3,4], [5,6], [7]]
function chunk(arr, size) {
  let target = [];
  let count = size;
  if (arr.length < size) {
    target.push(arr.slice());
  } else {
    let len = arr.length - arr.length % size;
    for (let i=0; i<len; i=i+size) {
      let newArr = arr.slice(i, count);
      count = count + size;
      target.push(newArr);
    }
    if (arr.length % size !== 0) {
      target.push(arr.slice(arr.length - arr.length % size));
    }
  }
  return target;
}

let target = chunk([1,2,3,4,5,6], 7);
console.log(target);

33、实现红绿灯

  • 要求使用一个 div 实现红绿灯效果,把一个圆形 div 按照绿色 3s,黄色 1s,红色 2s 这样循环改变颜色
#traffic-color {
  width: 100px;
  height: 100px;
  overflow: hidden;
  border-radius: 50%;
}
<div id="traffic-color"></div>

const TRAFFIC_COLOR_CONFIG = {
  'green': 3000,
  'yellow': 1000,
  'red': 2000
}
function delay(duration) {
  return new Promise((resolve, reject) => setTimeout(resolve, duration));
}

async function changeColor(color) {
  document.getElementById('traffic-color').style.background = color;
  await delay(TRAFFIC_COLOR_CONFIG[color]);
}

async function run() {
  // while(1) {
  //   await changeColor('green', 3000);
  //   await changeColor('yellow', 1000);
  //   await changeColor('red', 2000);
  // }
  // await changeColor('green', 3000);
  // await changeColor('yellow', 1000);
  // await changeColor('red', 2000);
  
  for (const key in TRAFFIC_COLOR_CONFIG) {
    await changeColor(key);
  }
  run()
}
run();

34、实现 indexOf()

注意几点:

  • 如果索引开始大于字符串/数组的长度时,返回 -1;
  • 如果索引开始小于等于 0,则按照从 0 开始索引;
  • 如果字符串或者数组长度为 0,返回 0;
  • 否则,返回 -1;
function indexOf(searchValue, index=0) {
  if (this.length < 1 || index > this.length) return -1;
  if (!searchValue) return 0;
  index = index <= 0 ? 0 : index;
  for (let i=0; i<this.length; i++) {
    if (this[i] === searchValue) {
      return i;
    }
  }
  return -1;
}
Array.prototype.myIndexOf = indexOf;
String.prototype.myIndexOf = indexOf;
console.log([1, 2, 3].myIndexOf(2));
console.log('123'.myIndexOf('2'));

35、写版本号排序的方法

  • 题目描述:有一组版本号如下['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5']。现在需要对其进行排序;
  • 排序的结果为 ['4.3.5', '4.3.4.5', '2.3.3', '0.302.1', '0.1.1'];
arr.sort((a, b) => {
  let i = 0;
  const arr1 = a.split(".");
  const arr2 = b.split(".");

  while (true) {
    const s1 = arr1[i];
    const s2 = arr2[i];
    i++;
    if (s1 === undefined || s2 === undefined) {
      return arr2.length - arr1.length;
    }

    if (s1 === s2) continue;

    return s2 - s1;
  }
});
console.log(arr);

36、类数组转化为数组的方法

  • 如何把类数组转化为数组?
const arrayLike=document.querySelectorAll('div')

// 1.扩展运算符
[...arrayLike]
// 2.Array.from
Array.from(arrayLike)
// 3.Array.prototype.slice
Array.prototype.slice.call(arrayLike)
// 4.Array.apply
Array.apply(null, arrayLike)
// 5.Array.prototype.concat
Array.prototype.concat.apply([], arrayLike)

37、实现 Object.is()

  • Object.is不会转换被比较的两个值的类型,这点和 === 更为相似,他们之间也存在一些区别。
      1. NaN 在 === 中是不相等的,而在 Object.is() 中是相等的;
      1. +0 和 -0 在 === 中是相等的,而在 Object.is() 中是不相等的;
Object.is = function (x, y) {
  if (x === y) {
    // 当前情况下,只有一种情况是特殊的,即 +0 -0
    // 如果 x !== 0,则返回true
    // 如果 x === 0,则需要判断+0和-0,则可以直接使用 1/+0 === Infinity 和 1/-0 === -Infinity来进行判断
    return x !== 0 || 1 / x === 1 / y;
  }

  // x !== y 的情况下,只需要判断是否为NaN,如果x!==x,则说明x是NaN,同理y也一样
  // x和y同时为NaN时,返回true
  return x !== x && y !== y;
};

38、虚拟 DOM 转化为真实 DOM

  • JSON 格式的虚拟 DOM 怎么转换成真实 DOM; 题目描述:
{
  tag: 'DIV',
  attrs:{
  id:'app'
  },
  children: [
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] }
      ]
    },
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] },
        { tag: 'A', children: [] }
      ]
    }
  ]
}
把上诉虚拟 `DOM` 转化成下方真实 `DOM`;
<div id="app">
  <span>
    <a></a>
  </span>
  <span>
    <a></a>
    <a></a>
  </span>
</div>

实现代码如下:

// 真正的渲染函数
function _render(vnode) {
  // 如果是数字类型转化为字符串
  if (typeof vnode === "number") {
    vnode = String(vnode);
  }
  // 字符串类型直接就是文本节点
  if (typeof vnode === "string") {
    return document.createTextNode(vnode);
  }
  // 普通DOM
  const dom = document.createElement(vnode.tag);
  if (vnode.attrs) {
    // 遍历属性
    Object.keys(vnode.attrs).forEach((key) => {
      const value = vnode.attrs[key];
      dom.setAttribute(key, value);
    });
  }
  // 子数组进行递归操作
  vnode.children.forEach((child) => dom.appendChild(_render(child)));
  return dom;
}

39、DOM 节点输出 JSON 的格式

题目描述:

<div>
  <span>
    <a></a>
  </span>
  <span>
    <a></a>
    <a></a>
  </span>
</div>

把上诉 `DOM` 结构转成下面的 `JSON` 格式
{
  tag: 'DIV',
  children: [
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] }
      ]
    },
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] },
        { tag: 'A', children: [] }
      ]
    }
  ]
}

实现代码如下:

function dom2Json(domtree) {
  let obj = {};
  obj.name = domtree.tagName;
  obj.children = [];
  domtree.childNodes.forEach((child) => obj.children.push(dom2Json(child)));
  return obj;
}

40、实现一个对象的 flatten 方法

题目描述:

const obj = {
  a: {
    b: 1,
    c: 2,
    d: {e: 5}
  },
  b: [1, 3, {a: 2, b: 3}],
  c: 3
}

flatten(obj) 结果返回如下
// {
//  'a.b': 1,
//  'a.c': 2,
//  'a.d.e': 5,
//  'b[0]': 1,
//  'b[1]': 3,
//  'b[2].a': 2,
//  'b[2].b': 3
//   c: 3
// }

实现代码如下:

function isObject(val) {
  return typeof val === "object" && val !== null;
}

function flatten(obj) {
  if (!isObject(obj)) {
    return;
  }
  let res = {};
  const dfs = (cur, prefix) => {
    if (isObject(cur)) {
      if (Array.isArray(cur)) {
        cur.forEach((item, index) => {
          dfs(item, `${prefix}[${index}]`);
        });
      } else {
        for (let k in cur) {
          dfs(cur[k], `${prefix}${prefix ? "." : ""}${k}`);
        }
      }
    } else {
      res[prefix] = cur;
    }
  };
  dfs(obj, "");

  return res;
}
flatten();

41、列表转成树形结构

题目描述:

[
  {
    id: 1,
    text: '节点1',
    parentId: 0 //这里用0表示为顶级节点
  },
  {
    id: 2,
    text: '节点1_1',
    parentId: 1 //通过这个字段来确定子父级
  }
  ...
]

转成
[
  {
    id: 1,
    text: '节点1',
    parentId: 0,
    children: [
      {
        id: 2,
        text: '节点1_1',
        parentId: 1
      }
    ]
  }
]

实现代码如下:

function listToTree(data) {
  let temp = {};
  let treeData = [];
  for (let i = 0; i < data.length; i++) {
    temp[data[i].id] = data[i];
  }
  for (let i in temp) {
    if (+temp[i].parentId != 0) {
      if (!temp[temp[i].parentId].children) {
        temp[temp[i].parentId].children = [];
      }
      temp[temp[i].parentId].children.push(temp[i]);
    } else {
      treeData.push(temp[i]);
    }
  }
  return treeData;
}

42、树形结构转成列表

题目描述:

[
  {
    id: 1,
    text: '节点1',
    parentId: 0,
    children: [
      {
        id:2,
        text: '节点1_1',
        parentId:1
      }
    ]
  }
]
转成
[
  {
    id: 1,
    text: '节点1',
    parentId: 0 //这里用0表示为顶级节点
  },
  {
    id: 2,
    text: '节点1_1',
    parentId: 1 //通过这个字段来确定子父级
  }
  ...
]

实现代码如下:

function treeToList(data) {
  let res = [];
  const dfs = (tree) => {
    tree.forEach((item) => {
      if (item.children) {
        dfs(item.children);
        delete item.children;
      }
      res.push(item);
    });
  };
  dfs(data);
  return res;
}

43、深度优先遍历

深度优先遍历——是指从某个顶点出发,首先访问这个顶点,然后找出刚访问这个结点的第一个未被访问的邻结点,然后再以此邻结点为顶点,继续找它的下一个顶点进行访问。重复此步骤,直至所有结点都被访问完为止。

深度优先遍历的递归写法

function deepTraversal(node) { 
  let nodes = [] 
  if (node != null) { 
    nodes.push(node) 
    let childrens = node.children 
    for (let i = 0; i < childrens.length; i++) {
      deepTraversal(childrens[i])
    }
  } 
  return nodes
}

深度优先遍历的非递归写法

function deepTraversal(node) { 
  let nodes = [] 
  if (node != null) {
    let stack = []
    // 同来存放将来要访问的节点 
    stack.push(node)
    while (stack.length != 0) { 
      let item = stack.pop()
      // 正在访问的节点 
      nodes.push(item) 
      let childrens = item.children 
      for ( let i = childrens.length - 1; i >= 0; i--) {
        // 将现在访问点的节点的子节点存入 stack,供将来访问 ) 
        stack.push(childrens[i])
      }
    }
  }
  return nodes
}

44、广度优先遍历

广度优先遍历——是从某个顶点出发,首先访问这个顶点,然后找出刚访问这个结点所有未被访问的邻结点,访问完后再访问这些结点中第一个邻结点的所有结点,重复此方法,直到所有结点都被访问完为止。

广度优先遍历的递归写法

function wideTraversal(node) { 
  let nodes = [], i = 0
  if (node != null) { 
    nodes.push(node) 
    wideTraversal(node.nextElementSibling) 
    node = nodes[i++] 
    wideTraversal(node.firstElementChild)
  } 
  return nodes
}

广度优先遍历的非递归写法

function wideTraversal(node) { 
  let nodes = [], i = 0 
  while (node != null) { 
    nodes.push(node) 
    node = nodes[i++] 
    let childrens = node.children 
    for (let i = 0; i < childrens.length; i++) { 
      nodes.push(childrens[i])
    }
  } 
  return nodes
}