关于javascript:前端手写面试题总结

8次阅读

共计 23290 个字符,预计需要花费 59 分钟才能阅读完成。

异步并发数限度

/**
 * 关键点
 * 1. new promise 一经创立,立刻执行
 * 2. 应用 Promise.resolve().then 能够把工作加到微工作队列,避免立刻执行迭代办法
 * 3. 微工作处理过程中,产生的新的微工作,会在同一事件循环内,追加到微工作队列里
 * 4. 应用 race 在某个工作实现时,持续增加工作,放弃工作依照最大并发数进行执行
 * 5. 工作实现后,须要从 doingTasks 中移出
 */
function limit(count, array, iterateFunc) {const tasks = []
  const doingTasks = []
  let i = 0
  const enqueue = () => {if (i === array.length) {return Promise.resolve()
    }
    const task = Promise.resolve().then(() => iterateFunc(array[i++]))
    tasks.push(task)
    const doing = task.then(() => doingTasks.splice(doingTasks.indexOf(doing), 1))
    doingTasks.push(doing)
    const res = doingTasks.length >= count ? Promise.race(doingTasks) : Promise.resolve()
    return res.then(enqueue)
  };
  return enqueue().then(() => Promise.all(tasks))
}

// test
const timeout = i => new Promise(resolve => setTimeout(() => resolve(i), i))
limit(2, [1000, 1000, 1000, 1000], timeout).then((res) => {console.log(res)
})

实现 some 办法

Array.prototype.mySome=function(callback, context = window){
             var len = this.length,
                 flag=false,
           i = 0;

             for(;i < len; i++){if(callback.apply(context, [this[i], i , this])){
                    flag=true;
                    break;
                } 
             }
             return flag;
        }

        // var flag=arr.mySome((v,index,arr)=>v.num>=10,obj)
        // console.log(flag);

实现 ES6 的 extends

function B(name){this.name = name;};
function A(name,age){
  //1. 将 A 的原型指向 B
  Object.setPrototypeOf(A,B);
  //2. 用 A 的实例作为 this 调用 B, 失去继承 B 之后的实例,这一步相当于调用 super
  Object.getPrototypeOf(A).call(this, name)
  //3. 将 A 原有的属性增加到新实例上
  this.age = age; 
  //4. 返回新实例对象
  return this;
};
var a = new A('poetry',22);
console.log(a);

实现 call 办法

call 做了什么:

  • 将函数设为对象的属性
  • 执行和删除这个函数
  • 指定 this 到函数并传入给定参数执行函数
  • 如果不传入参数,默认指向为 window
// 模仿 call bar.mycall(null);
// 实现一个 call 办法:// 原理:利用 context.xxx = self obj.xx = func-->obj.xx()
Function.prototype.myCall = function(context = window, ...args) {if (typeof this !== "function") {throw new Error('type error')
  }
  // this-->func  context--> obj  args--> 传递过去的参数

  // 在 context 上加一个惟一值不影响 context 上的属性
  let key = Symbol('key')
  context[key] = this; // context 为调用的上下文,this 此处为函数,将这个函数作为 context 的办法
  // let args = [...arguments].slice(1)   // 第一个参数为 obj 所以删除, 伪数组转为数组

  // 绑定参数 并执行函数
  let result = context[key](...args);
  // 革除定义的 this 不删除会导致 context 属性越来越多
  delete context[key];

  // 返回后果 
  return result;
};
// 用法:f.call(obj,arg1)
function f(a,b){console.log(a+b)
 console.log(this.name)
}
let obj={name:1}
f.myCall(obj,1,2) // 否则 this 指向 window

实现 map 办法

  • 回调函数的参数有哪些,返回值如何解决
  • 不批改原来的数组
Array.prototype.myMap = function(callback, context){
  // 转换类数组
  var arr = Array.prototype.slice.call(this),// 因为是 ES5 所以就不必... 开展符了
      mappedArr = [], 
      i = 0;

  for (; i < arr.length; i++){// 把以后值、索引、以后数组返回去。调用的时候传到函数参数中 [1,2,3,4].map((curr,index,arr))
    mappedArr.push(callback.call(context, arr[i], i, this));
  }
  return mappedArr;
}

实现 reduce 办法

  • 初始值不传怎么解决
  • 回调函数的参数有哪些,返回值如何解决。
Array.prototype.myReduce = function(fn, initialValue) {var arr = Array.prototype.slice.call(this);
  var res, startIndex;

  res = initialValue ? initialValue : arr[0]; // 不传默认取数组第一项
  startIndex = initialValue ? 0 : 1;

  for(var i = startIndex; i < arr.length; i++) {// 把初始值、以后值、索引、以后数组返回去。调用的时候传到函数参数中 [1,2,3,4].reduce((initVal,curr,index,arr))
    res = fn.call(null, res, arr[i], i, this); 
  }
  return res;
}

参考 前端进阶面试题具体解答

实现 Array.isArray 办法

Array.myIsArray = function(o) {return Object.prototype.toString.call(Object(o)) === '[object Array]';
};

console.log(Array.myIsArray([])); // true

实现节流函数(throttle)

节流函数原理: 指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的工夫才会执行回调函数。总结起来就是:事件,依照一段时间的距离来进行触发

像 dom 的拖拽,如果用消抖的话,就会呈现卡顿的感觉,因为只在进行的时候执行了一次,这个时候就应该用节流,在肯定工夫内屡次执行,会晦涩很多

手写简版

应用工夫戳的节流函数会在第一次触发事件时立刻执行,当前每过 wait 秒之后才执行一次,并且最初一次触发事件不会被执行

工夫戳形式:

// func 是用户传入须要防抖的函数
// wait 是等待时间
const throttle = (func, wait = 50) => {
  // 上一次执行该函数的工夫
  let lastTime = 0
  return function(...args) {
    // 以后工夫
    let now = +new Date()
    // 将以后工夫和上一次执行函数工夫比照
    // 如果差值大于设置的等待时间就执行函数
    if (now - lastTime > wait) {
      lastTime = now
      func.apply(this, args)
    }
  }
}

setInterval(throttle(() => {console.log(1)
  }, 500),
  1
)

定时器形式:

应用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最初一次进行触发后,还会再执行一次函数

function throttle(func, delay){
  var timer = null;
  returnfunction(){
    var context = this;
    var args = arguments;
    if(!timer){timer = setTimeout(function(){func.apply(context, args);
        timer = null;
      },delay);
    }
  }
}

实用场景:

  • DOM 元素的拖拽性能实现(mousemove
  • 搜寻联想(keyup
  • 计算鼠标挪动的间隔(mousemove
  • Canvas 模仿画板性能(mousemove
  • 监听滚动事件判断是否到页面底部主动加载更多
  • 拖拽场景:固定工夫内只执行一次,避免超高频次触发地位变动
  • 缩放场景:监控浏览器resize
  • 动画场景:防止短时间内屡次触发动画引起性能问题

总结

  • 函数防抖:将几次操作合并为一次操作进行。原理是保护一个计时器,规定在 delay 工夫后触发函数,然而在 delay 工夫内再次触发的话,就会勾销之前的计时器而从新设置。这样一来,只有最初一次操作能被触发。
  • 函数节流:使得肯定工夫内只触发一次函数。原理是通过判断是否达到肯定工夫来触发函数。

实现迭代器生成函数

咱们说 迭代器对象 全凭 迭代器生成函数 帮咱们生成。在 ES6 中,实现一个迭代器生成函数并不是什么难事儿,因为 ES6 早帮咱们思考好了全套的解决方案,内置了贴心的 生成器Generator)供咱们应用:

// 编写一个迭代器生成函数
function *iteratorGenerator() {
    yield '1 号选手'
    yield '2 号选手'
    yield '3 号选手'
}

const iterator = iteratorGenerator()

iterator.next()
iterator.next()
iterator.next()

丢进控制台,不负众望:

写一个生成器函数并没有什么难度,但在面试的过程中,面试官往往对生成器这种语法糖背地的实现逻辑更感兴趣。上面咱们要做的,不仅仅是写一个迭代器对象,而是用 ES5 去写一个可能生成迭代器对象的迭代器生成函数(解析在正文里):

// 定义生成器函数,入参是任意汇合
function iteratorGenerator(list) {
    // idx 记录以后拜访的索引
    var idx = 0
    // len 记录传入汇合的长度
    var len = list.length
    return {
        // 自定义 next 办法
        next: function() {
            // 如果索引还没有超出汇合长度,done 为 false
            var done = idx >= len
            // 如果 done 为 false,则能够持续取值
            var value = !done ? list[idx++] : undefined

            // 将以后值与遍历是否结束(done)返回
            return {
                done: done,
                value: value
            }
        }
    }
}

var iterator = iteratorGenerator(['1 号选手', '2 号选手', '3 号选手'])
iterator.next()
iterator.next()
iterator.next()

此处为了记录每次遍历的地位,咱们实现了一个闭包,借助自在变量来做咱们的迭代过程中的“游标”。

运行一下咱们自定义的迭代器,后果合乎预期:

实现数组的 map 办法

Array.prototype._map = function(fn) {if (typeof fn !== "function") {throw Error('参数必须是一个函数');
    }
    const res = [];
    for (let i = 0, len = this.length; i < len; i++) {res.push(fn(this[i]));
    }
    return res;
}

实现 Object.is

Object.is不会转换被比拟的两个值的类型,这点和 === 更为类似,他们之间也存在一些区别

  • NaN=== 中是不相等的,而在 Object.is 中是相等的
  • +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;
};

手写节流函数

函数节流是指规定一个单位工夫,在这个单位工夫内,只能有一次触发事件的回调函数执行,如果在同一个单位工夫内某事件被触发屡次,只有一次能失效。节流能够应用在 scroll 函数的事件监听上,通过事件节流来升高事件调用的频率。

// 函数节流的实现;
function throttle(fn, delay) {let curTime = Date.now();

  return function() {
    let context = this,
        args = arguments,
        nowTime = Date.now();

    // 如果两次工夫距离超过了指定工夫,则执行函数。if (nowTime - curTime >= delay) {curTime = Date.now();
      return fn.apply(context, args);
    }
  };
}

实现 Event(event bus)

event bus 既是 node 中各个模块的基石,又是前端组件通信的依赖伎俩之一,同时波及了订阅 - 公布设计模式,是十分重要的根底。

简略版:

class EventEmeitter {constructor() {this._events = this._events || new Map(); // 贮存事件 / 回调键值对
    this._maxListeners = this._maxListeners || 10; // 设立监听下限
  }
}


// 触发名为 type 的事件
EventEmeitter.prototype.emit = function(type, ...args) {
  let handler;
  // 从贮存事件键值对的 this._events 中获取对应事件回调函数
  handler = this._events.get(type);
  if (args.length > 0) {handler.apply(this, args);
  } else {handler.call(this);
  }
  return true;
};

// 监听名为 type 的事件
EventEmeitter.prototype.addListener = function(type, fn) {
  // 将 type 事件以及对应的 fn 函数放入 this._events 中贮存
  if (!this._events.get(type)) {this._events.set(type, fn);
  }
};

面试版:

class EventEmeitter {constructor() {this._events = this._events || new Map(); // 贮存事件 / 回调键值对
    this._maxListeners = this._maxListeners || 10; // 设立监听下限
  }
}

// 触发名为 type 的事件
EventEmeitter.prototype.emit = function(type, ...args) {
  let handler;
  // 从贮存事件键值对的 this._events 中获取对应事件回调函数
  handler = this._events.get(type);
  if (args.length > 0) {handler.apply(this, args);
  } else {handler.call(this);
  }
  return true;
};

// 监听名为 type 的事件
EventEmeitter.prototype.addListener = function(type, fn) {
  // 将 type 事件以及对应的 fn 函数放入 this._events 中贮存
  if (!this._events.get(type)) {this._events.set(type, fn);
  }
};

// 触发名为 type 的事件
EventEmeitter.prototype.emit = function(type, ...args) {
  let handler;
  handler = this._events.get(type);
  if (Array.isArray(handler)) {
    // 如果是一个数组阐明有多个监听者, 须要顺次此触发外面的函数
    for (let i = 0; i < handler.length; i++) {if (args.length > 0) {handler[i].apply(this, args);
      } else {handler[i].call(this);
      }
    }
  } else {
    // 单个函数的状况咱们间接触发即可
    if (args.length > 0) {handler.apply(this, args);
    } else {handler.call(this);
    }
  }

  return true;
};

// 监听名为 type 的事件
EventEmeitter.prototype.addListener = function(type, fn) {const handler = this._events.get(type); // 获取对应事件名称的函数清单
  if (!handler) {this._events.set(type, fn);
  } else if (handler && typeof handler === "function") {
    // 如果 handler 是函数阐明只有一个监听者
    this._events.set(type, [handler, fn]); // 多个监听者咱们须要用数组贮存
  } else {handler.push(fn); // 曾经有多个监听者, 那么间接往数组里 push 函数即可
  }
};

EventEmeitter.prototype.removeListener = function(type, fn) {const handler = this._events.get(type); // 获取对应事件名称的函数清单

  // 如果是函数, 阐明只被监听了一次
  if (handler && typeof handler === "function") {this._events.delete(type, fn);
  } else {
    let postion;
    // 如果 handler 是数组, 阐明被监听屡次要找到对应的函数
    for (let i = 0; i < handler.length; i++) {if (handler[i] === fn) {postion = i;} else {postion = -1;}
    }
    // 如果找到匹配的函数, 从数组中革除
    if (postion !== -1) {
      // 找到数组对应的地位, 间接革除此回调
      handler.splice(postion, 1);
      // 如果革除后只有一个函数, 那么勾销数组, 以函数模式保留
      if (handler.length === 1) {this._events.set(type, handler[0]);
      }
    } else {return this;}
  }
};

实现具体过程和思路见实现 event

实现数组元素求和

  • arr=[1,2,3,4,5,6,7,8,9,10],求和
let arr=[1,2,3,4,5,6,7,8,9,10]
let sum = arr.reduce((total,i) => total += i,0);
console.log(sum);
  • arr=[1,2,3,[[4,5],6],7,8,9],求和
var = arr=[1,2,3,[[4,5],6],7,8,9]
let arr= arr.toString().split(',').reduce((total,i) => total += Number(i),0);
console.log(arr);

递归实现:

let arr = [1, 2, 3, 4, 5, 6] 

function add(arr) {if (arr.length == 1) return arr[0] 
    return arr[0] + add(arr.slice(1)) 
}
console.log(add(arr)) // 21

实现类的继承

实现类的继承 - 简版

类的继承在几年前是重点内容,有 n 种继承形式各有优劣,es6 遍及后越来越不重要,那么多种写法有点『回字有四样写法』的意思,如果还想深刻了解的去看红宝书即可,咱们目前只实现一种最现实的继承形式。

// 寄生组合继承
function Parent(name) {this.name = name}
Parent.prototype.say = function() {console.log(this.name + ` say`);
}
Parent.prototype.play = function() {console.log(this.name + ` play`);
}

function Child(name, parent) {
  // 将父类的构造函数绑定在子类上
  Parent.call(this, parent)
  this.name = name
}

/** 
 1. 这一步不必 Child.prototype = Parent.prototype 的起因是怕共享内存,批改父类原型对象就会影响子类
 2. 不必 Child.prototype = new Parent()的起因是会调用 2 次父类的构造方法(另一次是 call),会存在一份多余的父类实例属性
3. Object.create 是创立了父类原型的正本,与父类原型齐全隔离
*/
Child.prototype = Object.create(Parent.prototype);
Child.prototype.say = function() {console.log(this.name + ` say`);
}

// 留神记得把子类的结构指向子类自身
Child.prototype.constructor = Child;
// 测试
var parent = new Parent('parent');
parent.say() 

var child = new Child('child');
child.say() 
child.play(); // 继承父类的办法

ES5 实现继承 - 具体

第一种形式是借助 call 实现继承

function Parent1(){this.name = 'parent1';}
function Child1(){Parent1.call(this);
    this.type = 'child1'    
}
console.log(new Child1);

这样写的时候子类尽管可能拿到父类的属性值,然而问题是父类中一旦存在办法那么子类无奈继承。那么引出上面的办法

第二种形式借助原型链实现继承:

function Parent2() {
    this.name = 'parent2';
    this.play = [1, 2, 3]
  }
  function Child2() {this.type = 'child2';}
  Child2.prototype = new Parent2();

  console.log(new Child2());

看似没有问题,父类的办法和属性都可能拜访,但实际上有一个潜在的有余。举个例子:

var s1 = new Child2();
  var s2 = new Child2();
  s1.play.push(4);
  console.log(s1.play, s2.play); // [1,2,3,4] [1,2,3,4]

明明我只扭转了 s1 的 play 属性,为什么 s2 也跟着变了呢?很简略,因为两个实例应用的是同一个原型对象

第三种形式:将前两种组合:

function Parent3 () {
    this.name = 'parent3';
    this.play = [1, 2, 3];
  }
  function Child3() {Parent3.call(this);
    this.type = 'child3';
  }
  Child3.prototype = new Parent3();
  var s3 = new Child3();
  var s4 = new Child3();
  s3.play.push(4);
  console.log(s3.play, s4.play); // [1,2,3,4] [1,2,3]

之前的问题都得以解决。然而这里又徒增了一个新问题,那就是 Parent3 的构造函数会多执行了一次(Child3.prototype = new Parent3();)。这是咱们不愿看到的。那么如何解决这个问题?

第四种形式: 组合继承的优化 1

function Parent4 () {
    this.name = 'parent4';
    this.play = [1, 2, 3];
  }
  function Child4() {Parent4.call(this);
    this.type = 'child4';
  }
  Child4.prototype = Parent4.prototype;

这里让将父类原型对象间接给到子类,父类构造函数只执行一次,而且父类属性和办法均能拜访,然而咱们来测试一下

var s3 = new Child4();
  var s4 = new Child4();
  console.log(s3)

子类实例的构造函数是 Parent4,显然这是不对的,应该是 Child4。

第五种形式(最举荐应用):优化 2

function Parent5 () {
    this.name = 'parent5';
    this.play = [1, 2, 3];
  }
  function Child5() {Parent5.call(this);
    this.type = 'child5';
  }
  Child5.prototype = Object.create(Parent5.prototype);
  Child5.prototype.constructor = Child5;

这是最举荐的一种形式,靠近完满的继承。

实现 Array.of 办法

Array.of()办法用于将一组值,转换为数组

  • 这个办法的次要目标,是补救数组构造函数 Array() 的有余。因为参数个数的不同,会导致 Array() 的行为有差别。
  • Array.of()基本上能够用来代替 Array()new Array(),并且不存在因为参数不同而导致的重载。它的行为十分对立
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1

实现

function ArrayOf(){return [].slice.call(arguments);
}

实现深拷贝

简洁版本

简略版:

const newObj = JSON.parse(JSON.stringify(oldObj));

局限性:

  • 他无奈实现对函数、RegExp 等非凡对象的克隆
  • 会摈弃对象的constructor, 所有的构造函数会指向Object
  • 对象有循环援用, 会报错

面试简版

function deepClone(obj) {
    // 如果是 值类型 或 null,则间接 return
    if(typeof obj !== 'object' || obj === null) {return obj}

    // 定义后果对象
    let copy = {}

    // 如果对象是数组,则定义后果数组
    if(obj.constructor === Array) {copy = []
    }

    // 遍历对象的 key
    for(let key in obj) {
        // 如果 key 是对象的自有属性
        if(obj.hasOwnProperty(key)) {
          // 递归调用深拷贝办法
          copy[key] = deepClone(obj[key])
        }
    }

    return copy
} 

调用深拷贝办法,若属性为值类型,则间接返回;若属性为援用类型,则递归遍历。这就是咱们在解这一类题时的外围的办法。

进阶版

  • 解决拷贝循环援用问题
  • 解决拷贝对应原型问题
// 递归拷贝 (类型判断)
function deepClone(value,hash = new WeakMap){ // 弱援用,不必 map,weakMap 更适合一点
  // null 和 undefiend 是不须要拷贝的
  if(value == null){return value;}
  if(value instanceof RegExp) {return new RegExp(value) }
  if(value instanceof Date) {return new Date(value) }
  // 函数是不须要拷贝
  if(typeof value != 'object') return value;
  let obj = new value.constructor(); // [] {}
  // 阐明是一个对象类型
  if(hash.get(value)){return hash.get(value)
  }
  hash.set(value,obj);
  for(let key in value){ // in 会遍历以后对象上的属性 和 __proto__指代的属性
    // 补拷贝 对象的__proto__上的属性
    if(value.hasOwnProperty(key)){
      // 如果值还有可能是对象 就持续拷贝
      obj[key] = deepClone(value[key],hash);
    }
  }
  return obj
  // 辨别对象和数组 Object.prototype.toString.call
}
// test

var o = {};
o.x = o;
var o1 = deepClone(o); // 如果这个对象拷贝过了 就返回那个拷贝的后果就能够了
console.log(o1);

实现残缺的深拷贝

1. 简易版及问题

JSON.parse(JSON.stringify());

预计这个 api 能笼罩大多数的利用场景,没错,谈到深拷贝,我第一个想到的也是它。然而实际上,对于某些严格的场景来说,这个办法是有微小的坑的。问题如下:

  1. 无奈解决 循环援用 的问题。举个例子:
const a = {val:2};
a.target = a;

拷贝 a 会呈现零碎栈溢出,因为呈现了有限递归的状况。

  1. 无奈拷贝一些非凡的对象,诸如 RegExp, Date, Set, Map
  2. 无奈拷贝 函数(划重点)。

因而这个 api 先 pass 掉,咱们从新写一个深拷贝,简易版如下:

const deepClone = (target) => {if (typeof target === 'object' && target !== null) {const cloneTarget = Array.isArray(target) ? []: {};
    for (let prop in target) {if (target.hasOwnProperty(prop)) {cloneTarget[prop] = deepClone(target[prop]);
      }
    }
    return cloneTarget;
  } else {return target;}
}

当初,咱们以刚刚发现的三个问题为导向,一步步来欠缺、优化咱们的深拷贝代码。

2. 解决循环援用

当初问题如下:

let obj = {val : 100};
obj.target = obj;

deepClone(obj);// 报错: RangeError: Maximum call stack size exceeded

这就是循环援用。咱们怎么来解决这个问题呢?

创立一个 Map。记录下曾经拷贝过的对象,如果说曾经拷贝过,那间接返回它行了。

const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;

const deepClone = (target, map = new Map()) => {if(map.get(target))  
    return target; 


  if (isObject(target)) {map.set(target, true); 
    const cloneTarget = Array.isArray(target) ? []: {}; 
    for (let prop in target) {if (target.hasOwnProperty(prop)) {cloneTarget[prop] = deepClone(target[prop],map); 
      } 
    } 
    return cloneTarget; 
  } else {return target;} 
}

当初来试一试:

const a = {val:2};
a.target = a;
let newA = deepClone(a);
console.log(newA)//{val: 2, target: { val: 2, target: [Circular] } }

如同是没有问题了, 拷贝也实现了。但还是有一个潜在的坑, 就是 map 上的 key 和 map 形成了强援用关系,这是相当危险的。我给你解释一下与之绝对的弱援用的概念你就明确了

在计算机程序设计中,弱援用与强援用绝对,

被弱援用的对象能够在任何时候被回收,而对于强援用来说,只有这个强援用还在,那么对象无奈被回收。拿下面的例子说,map 和 a 始终是强援用的关系,在程序完结之前,a 所占的内存空间始终不会被开释。

怎么解决这个问题?

很简略,让 map 的 key 和 map 形成弱援用即可。ES6 给咱们提供了这样的数据结构,它的名字叫 WeakMap,它是一种非凡的 Map, 其中的键是弱援用的。其键必须是对象,而值能够是任意的

略微革新一下即可:

const deepClone = (target, map = new WeakMap()) => {//...}

3. 拷贝非凡对象

可持续遍历

对于非凡的对象,咱们应用以下形式来甄别:

Object.prototype.toString.call(obj);

梳理一下对于可遍历对象会有什么后果:

["object Map"]
["object Set"]
["object Array"]
["object Object"]
["object Arguments"]

以这些不同的字符串为根据,咱们就能够胜利地甄别这些对象。

const getType = Object.prototype.toString.call(obj);

const canTraverse = {'[object Map]': true,
  '[object Set]': true,
  '[object Array]': true,
  '[object Object]': true,
  '[object Arguments]': true,
};

const deepClone = (target, map = new Map()) => {if(!isObject(target)) 
    return target;
  let type = getType(target);
  let cloneTarget;
  if(!canTraverse[type]) {
    // 解决不能遍历的对象
    return;
  }else {
    // 这波操作相当要害,能够保障对象的原型不失落!let ctor = target.prototype;
    cloneTarget = new ctor();}

  if(map.get(target)) 
    return target;
  map.put(target, true);

  if(type === mapTag) {
    // 解决 Map
    target.forEach((item, key) => {cloneTarget.set(deepClone(key), deepClone(item));
    })
  }

  if(type === setTag) {
    // 解决 Set
    target.forEach(item => {target.add(deepClone(item));
    })
  }

  // 解决数组和对象
  for (let prop in target) {if (target.hasOwnProperty(prop)) {cloneTarget[prop] = deepClone(target[prop]);
    }
  }
  return cloneTarget;
}

不可遍历的对象

const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

对于不可遍历的对象,不同的对象有不同的解决。

const handleRegExp = (target) => {const { source, flags} = target;
  return new target.constructor(source, flags);
}

const handleFunc = (target) => {// 待会的重点局部}

const handleNotTraverse = (target, tag) => {
  const Ctor = targe.constructor;
  switch(tag) {
    case boolTag:
    case numberTag:
    case stringTag:
    case errorTag: 
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
}

4. 拷贝函数

  • 尽管函数也是对象,然而它过于非凡,咱们独自把它拿进去拆解。
  • 提到函数,在 JS 种有两种函数,一种是一般函数,另一种是箭头函数。每个一般函数都是
  • Function 的实例,而箭头函数不是任何类的实例,每次调用都是不一样的援用。那咱们只须要
  • 解决一般函数的状况,箭头函数间接返回它自身就好了。

那么如何来辨别两者呢?

答案是: 利用原型。箭头函数是不存在原型的。

const handleFunc = (func) => {
  // 箭头函数间接返回本身
  if(!func.prototype) return func;
  const bodyReg = /(?<={)(.|\n)+(?=})/m;
  const paramReg = /(?<=\().+(?=\)\s+{)/;
  const funcString = func.toString();
  // 别离匹配 函数参数 和 函数体
  const param = paramReg.exec(funcString);
  const body = bodyReg.exec(funcString);
  if(!body) return null;
  if (param) {const paramArr = param[0].split(',');
    return new Function(...paramArr, body[0]);
  } else {return new Function(body[0]);
  }
}

5. 残缺代码展现

const getType = obj => Object.prototype.toString.call(obj);

const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;

const canTraverse = {'[object Map]': true,
  '[object Set]': true,
  '[object Array]': true,
  '[object Object]': true,
  '[object Arguments]': true,
};
const mapTag = '[object Map]';
const setTag = '[object Set]';
const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

const handleRegExp = (target) => {const { source, flags} = target;
  return new target.constructor(source, flags);
}

const handleFunc = (func) => {
  // 箭头函数间接返回本身
  if(!func.prototype) return func;
  const bodyReg = /(?<={)(.|\n)+(?=})/m;
  const paramReg = /(?<=\().+(?=\)\s+{)/;
  const funcString = func.toString();
  // 别离匹配 函数参数 和 函数体
  const param = paramReg.exec(funcString);
  const body = bodyReg.exec(funcString);
  if(!body) return null;
  if (param) {const paramArr = param[0].split(',');
    return new Function(...paramArr, body[0]);
  } else {return new Function(body[0]);
  }
}

const handleNotTraverse = (target, tag) => {
  const Ctor = target.constructor;
  switch(tag) {
    case boolTag:
      return new Object(Boolean.prototype.valueOf.call(target));
    case numberTag:
      return new Object(Number.prototype.valueOf.call(target));
    case stringTag:
      return new Object(String.prototype.valueOf.call(target));
    case symbolTag:
      return new Object(Symbol.prototype.valueOf.call(target));
    case errorTag: 
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
}

const deepClone = (target, map = new WeakMap()) => {if(!isObject(target)) 
    return target;
  let type = getType(target);
  let cloneTarget;
  if(!canTraverse[type]) {
    // 解决不能遍历的对象
    return handleNotTraverse(target, type);
  }else {
    // 这波操作相当要害,能够保障对象的原型不失落!let ctor = target.constructor;
    cloneTarget = new ctor();}

  if(map.get(target)) 
    return target;
  map.set(target, true);

  if(type === mapTag) {
    // 解决 Map
    target.forEach((item, key) => {cloneTarget.set(deepClone(key, map), deepClone(item, map));
    })
  }

  if(type === setTag) {
    // 解决 Set
    target.forEach(item => {cloneTarget.add(deepClone(item, map));
    })
  }

  // 解决数组和对象
  for (let prop in target) {if (target.hasOwnProperty(prop)) {cloneTarget[prop] = deepClone(target[prop], map);
    }
  }
  return cloneTarget;
}

实现 prototype 继承

所谓的原型链继承就是让新实例的原型等于父类的实例:

// 父办法
function SupperFunction(flag1){this.flag1 = flag1;}

// 子办法
function SubFunction(flag2){this.flag2 = flag2;}

// 父实例
var superInstance = new SupperFunction(true);

// 子继承父
SubFunction.prototype = superInstance;

// 子实例
var subInstance = new SubFunction(false);
// 子调用本人和父的属性
subInstance.flag1;   // true
subInstance.flag2;   // false

实现 AJAX 申请

AJAX 是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新以后网页的对应局部,而不必刷新整个网页。

创立 AJAX 申请的步骤:

  • 创立一个 XMLHttpRequest 对象。
  • 在这个对象上 应用 open 办法创立一个 HTTP 申请,open 办法所须要的参数是申请的办法、申请的地址、是否异步和用户的认证信息。
  • 在发动申请前,能够为这个对象 增加一些信息和监听函数。比如说能够通过 setRequestHeader 办法来为申请增加头信息。还能够为这个对象增加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变动时会触发 onreadystatechange 事件,能够通过设置监听函数,来解决申请胜利后的后果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接管实现,这个时候能够通过判断申请的状态,如果状态是 2xx 或者 304 的话则代表返回失常。这个时候就能够通过 response 中的数据来对页面进行更新了。
  • 当对象的属性和监听函数设置实现后,最初调 用 sent 办法来向服务器发动申请,能够传入参数作为发送的数据体。
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创立 Http 申请
xhr.open("GET", SERVER_URL, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {if (this.readyState !== 4) return;
  // 当申请胜利时
  if (this.status === 200) {handle(this.response);
  } else {console.error(this.statusText);
  }
};
// 设置申请失败时的监听函数
xhr.onerror = function() {console.error(this.statusText);
};
// 设置申请头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 申请
xhr.send(null);

实现 async/await

剖析

// generator 生成器  生成迭代器 iterator

// 默认这样写的类数组是不能被迭代的,短少迭代办法
let likeArray = {'0': 1, '1': 2, '2': 3, '3': 4, length: 4}

// // 应用迭代器使得能够开展数组
// // Symbol 有很多元编程办法,能够改 js 自身性能
// likeArray[Symbol.iterator] = function () {//   // 迭代器是一个对象 对象中有 next 办法 每次调用 next 都须要返回一个对象 {value,done}
//   let index = 0
//   return {//     next: ()=>{
//       // 会主动调用这个办法
//       console.log('index',index)
//       return {
//         // this 指向 likeArray
//         value: this[index],
//         done: index++ === this.length
//       }
//     }
//   }
// }
// let arr = [...likeArray]

// console.log('arr', arr)

// 应用生成器返回迭代器
// likeArray[Symbol.iterator] = function *() {
//   let index = 0
//   while (index != this.length) {//     yield this[index++]
//   }
// }
// let arr = [...likeArray]

// console.log('arr', arr)


// 生成器 碰到 yield 就会暂停
// function *read(params) {
//   yield 1;
//   yield 2;
// }
// 生成器返回的是迭代器
// let it = read()
// console.log(it.next())
// console.log(it.next())
// console.log(it.next())

// 通过 generator 来优化 promise(promise 的毛病是不停的链式调用)const fs = require('fs')
const path = require('path')
// const co = require('co') // 帮咱们执行 generator

const promisify = fn=>{return (...args)=>{return new Promise((resolve,reject)=>{fn(...args, (err,data)=>{if(err) {reject(err)
        } 
        resolve(data)
      })
    })
  }
}

// promise 化
let asyncReadFile = promisify(fs.readFile)

function * read() {let content1 = yield asyncReadFile(path.join(__dirname,'./data/name.txt'),'utf8')
  let content2 = yield asyncReadFile(path.join(__dirname,'./data/' + content1),'utf8')
  return content2
}

// 这样写太繁琐 须要借助 co 来实现
// let re = read()
// let {value,done} = re.next()
// value.then(data=>{
//   // 除了第一次传参没有意义外 剩下的传参都赋予了上一次的返回值 
//   let {value,done} = re.next(data) 
//   value.then(d=>{//     let {value,done} = re.next(d)
//     console.log(value,done)
//   })
// }).catch(err=>{//   re.throw(err) // 手动抛出谬误 能够被 try catch 捕捉
// })



// 实现 co 原理
function co(it) {// it 迭代器
  return new Promise((resolve,reject)=>{
    // 异步迭代 须要依据函数来实现
    function next(data) {
      // 递归得有停止条件
      let {value,done} = it.next(data)
      if(done) {resolve(value) // 间接让 promise 变成胜利 用以后返回的后果
      } else {// Promise.resolve(value).then(data=>{//   next(data)
        // }).catch(err=>{//   reject(err)
        // })
        // 简写
        Promise.resolve(value).then(next,reject)
      }
    }
    // 首次调用
    next()})
}

co(read()).then(d=>{console.log(d)
}).catch(err=>{console.log(err,'--')
})

整体看一下构造

function asyncToGenerator(generatorFunc) {return function() {const gen = generatorFunc.apply(this, arguments)
      return new Promise((resolve, reject) => {function step(key, arg) {
          let generatorResult
          try {generatorResult = gen[key](arg)
          } catch (error) {return reject(error)
          }
          const {value, done} = generatorResult
          if (done) {return resolve(value)
          } else {return Promise.resolve(value).then(val => step('next', val), err => step('throw', err))
          }
        }
        step("next")
      })
    }
}

剖析

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")
    })
  }
}
正文完
 0