乐趣区

关于前端:百度前端必会手写面试题及答案

实现一个 JSON.stringify

JSON.stringify(value[, replacer [, space]]):
  • Boolean | Number| String类型会主动转换成对应的原始值。
  • undefined、任意函数以及symbol,会被疏忽(呈现在非数组对象的属性值中时),或者被转换成 null(呈现在数组中时)。
  • 不可枚举的属性会被疏忽如果一个对象的属性值通过某种间接的形式指回该对象自身,即循环援用,属性也会被疏忽
  • 如果一个对象的属性值通过某种间接的形式指回该对象自身,即循环援用,属性也会被疏忽
function jsonStringify(obj) {
    let type = typeof obj;
    if (type !== "object") {if (/string|undefined|function/.test(type)) {obj = '"'+ obj +'"';}
        return String(obj);
    } else {let json = []
        let arr = Array.isArray(obj)
        for (let k in obj) {let v = obj[k];
            let type = typeof v;
            if (/string|undefined|function/.test(type)) {v = '"'+ v +'"';} else if (type === "object") {v = jsonStringify(v);
            }
            json.push((arr ? "":'"' + k + '":') + String(v));
        }
        return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}")
    }
}
jsonStringify({x : 5}) // "{"x":5}"
jsonStringify([1, "false", false]) // "[1,"false",false]"
jsonStringify({b: undefined}) // "{"b":"undefined"}"

前端手写面试题具体解答

实现防抖函数(debounce)

防抖函数原理:把触发十分频繁的事件合并成一次去执行 在指定工夫内只执行一次回调函数,如果在指定的工夫内又触发了该事件,则回调函数的执行工夫会基于此刻从新开始计算

防抖动和节流实质是不一样的。防抖动是将屡次执行变为 最初一次执行 ,节流是将屡次执行变成 每隔一段时间执行

eg. 像百度搜寻,就应该用防抖,当我连续不断输出时,不会发送申请;当我一段时间内不输出了,才会发送一次申请;如果小于这段时间持续输出的话,工夫会从新计算,也不会发送申请。

手写简化版:

// func 是用户传入须要防抖的函数
// wait 是等待时间
const debounce = (func, wait = 50) => {
  // 缓存一个定时器 id
  let timer = 0
  // 这里返回的函数是每次用户理论调用的防抖函数
  // 如果曾经设定过定时器了就清空上一次的定时器
  // 开始一个新的定时器,提早执行用户传入的办法
  return function(...args) {if (timer) clearTimeout(timer)
    timer = setTimeout(() => {func.apply(this, args)
    }, wait)
  }
}

实用场景:

  • 文本输出的验证,间断输出文字后发送 AJAX 申请进行验证,验证一次就好
  • 按钮提交场景:避免屡次提交按钮,只执行最初提交的一次
  • 服务端验证场景:表单验证须要服务端配合,只执行一段间断的输出事件的最初一次,还有搜寻联想词性能相似

实现节流函数(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 工夫内再次触发的话,就会勾销之前的计时器而从新设置。这样一来,只有最初一次操作能被触发。
  • 函数节流:使得肯定工夫内只触发一次函数。原理是通过判断是否达到肯定工夫来触发函数。

实现 Array.isArray 办法

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

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

解析 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;
}

分片思维解决大数据量渲染问题

题目形容: 渲染百万条构造简略的大数据时 怎么应用分片思维优化渲染

let ul = document.getElementById("container");
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
// 总页数
let page = total / once;
// 每条记录的索引
let index = 0;
// 循环加载数据
function loop(curTotal, curIndex) {if (curTotal <= 0) {return false;}
  // 每页多少条
  let pageCount = Math.min(curTotal, once);
  window.requestAnimationFrame(function () {for (let i = 0; i < pageCount; i++) {let li = document.createElement("li");
      li.innerText = curIndex + i + ":" + ~~(Math.random() * total);
      ul.appendChild(li);
    }
    loop(curTotal - pageCount, curIndex + pageCount);
  });
}
loop(total, index);

扩大思考:对于大数据量的简略 dom 构造渲染能够用分片思维解决 如果是简单的 dom 构造渲染如何解决?

这时候就须要应用 虚构列表 了,虚构列表和虚构表格在日常我的项目应用还是很多的

判断对象是否存在循环援用

循环援用对象原本没有什么问题,然而序列化的时候就会产生问题,比方调用 JSON.stringify() 对该类对象进行序列化,就会报错: Converting circular structure to JSON.

上面办法能够用来判断一个对象中是否已存在循环援用:

const isCycleObject = (obj,parent) => {const parentArr = parent || [obj];
    for(let i in obj) {if(typeof obj[i] === 'object') {
            let flag = false;
            parentArr.forEach((pObj) => {if(pObj === obj[i]){flag = true;}
            })
            if(flag) return true;
            flag = isCycleObject(obj[i],[...parentArr,obj[i]]);
            if(flag) return true;
        }
    }
    return false;
}


const a = 1;
const b = {a};
const c = {b};
const o = {d:{a:3},c}
o.c.b.aa = a;

console.log(isCycleObject(o)

查找有序二维数组的目标值:

var findNumberIn2DArray = function(matrix, target) {if (matrix == null || matrix.length == 0) {return false;}
    let row = 0;
    let column = matrix[0].length - 1;
    while (row < matrix.length && column >= 0) {if (matrix[row][column] == target) {return true;} else if (matrix[row][column] > target) {column--;} else {row++;}
    }
    return false;
};

二维数组斜向打印:

function printMatrix(arr){let m = arr.length, n = arr[0].length
    let res = []

  // 左上角,从 0 到 n - 1 列进行打印
  for (let k = 0; k < n; k++) {for (let i = 0, j = k; i < m && j >= 0; i++, j--) {res.push(arr[i][j]);
    }
  }

  // 右下角,从 1 到 n - 1 行进行打印
  for (let k = 1; k < m; k++) {for (let i = k, j = n - 1; i < m && j >= 0; i++, j--) {res.push(arr[i][j]);
    }
  }
  return res
}

实现公布 - 订阅模式

class EventCenter{
  // 1. 定义事件容器,用来装事件数组
    let handlers = {}

  // 2. 增加事件办法,参数:事件名 事件办法
  addEventListener(type, handler) {
    // 创立新数组容器
    if (!this.handlers[type]) {this.handlers[type] = []}
    // 存入事件
    this.handlers[type].push(handler)
  }

  // 3. 触发事件,参数:事件名 事件参数
  dispatchEvent(type, params) {
    // 若没有注册该事件则抛出谬误
    if (!this.handlers[type]) {return new Error('该事件未注册')
    }
    // 触发事件
    this.handlers[type].forEach(handler => {handler(...params)
    })
  }

  // 4. 事件移除,参数:事件名 要删除事件,若无第二个参数则删除该事件的订阅和公布
  removeEventListener(type, handler) {if (!this.handlers[type]) {return new Error('事件有效')
    }
    if (!handler) {
      // 移除事件
      delete this.handlers[type]
    } else {const index = this.handlers[type].findIndex(el => el === handler)
      if (index === -1) {return new Error('无该绑定事件')
      }
      // 移除事件
      this.handlers[type].splice(index, 1)
      if (this.handlers[type].length === 0) {delete this.handlers[type]
      }
    }
  }
}

instanceof

instanceof运算符用于检测构造函数的 prototype 属性是否呈现在某个实例对象的原型链上。

const myInstanceof = (left, right) => {
  // 根本数据类型都返回 false
  if (typeof left !== 'object' || left === null) return false;
  let proto = Object.getPrototypeOf(left);
  while (true) {if (proto === null) return false;
    if (proto === right.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
}

Promise.all

Promise.all是反对链式调用的,实质上就是返回了一个 Promise 实例,通过 resolvereject来扭转实例状态。

Promise.myAll = function(promiseArr) {return new Promise((resolve, reject) => {const ans = [];
    let index = 0;
    for (let i = 0; i < promiseArr.length; i++) {promiseArr[i]
      .then(res => {ans[i] = res;
        index++;
        if (index === promiseArr.length) {resolve(ans);
        }
      })
      .catch(err => reject(err));
    }
  })
}

实现单例模式

外围要点: 用闭包和 Proxy 属性拦挡

function proxy(func) {
    let instance;
    let handler = {constructor(target, args) {if(!instance) {instance = Reflect.constructor(fun, args);
            }
            return instance;
        }
    }
    return new Proxy(func, handler);
}

实现一个 call

call 做了什么:

  • 将函数设为对象的属性
  • 执行 & 删除这个函数
  • 指定 this 到函数并传入给定参数执行函数
  • 如果不传入参数,默认指向为 window
// 模仿 call bar.mycall(null);
// 实现一个 call 办法:Function.prototype.myCall = function(context) {
  // 此处没有思考 context 非 object 状况
  context.fn = this;
  let args = [];
  for (let i = 1, len = arguments.length; i < len; i++) {args.push(arguments[i]);
  }
  context.fn(...args);
  let result = context.fn(...args);
  delete context.fn;
  return result;
};

实现深拷贝

  • 浅拷贝: 浅拷贝指的是将一个对象的属性值复制到另一个对象,如果有的属性的值为援用类型的话,那么会将这个援用的地址复制给对象,因而两个对象会有同一个援用类型的援用。浅拷贝能够应用 Object.assign 和开展运算符来实现。
  • 深拷贝: 深拷贝绝对浅拷贝而言,如果遇到属性值为援用类型的时候,它新建一个援用类型并将对应的值复制给它,因而对象取得的一个新的援用类型而不是一个原有类型的援用。深拷贝对于一些对象能够应用 JSON 的两个函数来实现,然而因为 JSON 的对象格局比 js 的对象格局更加严格,所以如果属性值里边呈现函数或者 Symbol 类型的值时,会转换失败

(1)JSON.stringify()

  • JSON.parse(JSON.stringify(obj))是目前比拟罕用的深拷贝办法之一,它的原理就是利用 JSON.stringifyjs 对象序列化(JSON 字符串),再应用 JSON.parse 来反序列化 (还原)js 对象。
  • 这个办法能够简略粗犷的实现深拷贝,然而还存在问题,拷贝的对象中如果有函数,undefined,symbol,当应用过 JSON.stringify() 进行解决之后,都会隐没。
let obj1 = {  a: 0,
              b: {c: 0}
            };
let obj2 = JSON.parse(JSON.stringify(obj1));
obj1.a = 1;
obj1.b.c = 1;
console.log(obj1); // {a: 1, b: {c: 1}}
console.log(obj2); // {a: 0, b: {c: 0}}

(2)函数库 lodash 的_.cloneDeep 办法

该函数库也有提供_.cloneDeep 用来做 Deep Copy

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: {f: { g: 1} },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false

(3)手写实现深拷贝函数

// 深拷贝的实现
function deepCopy(object) {if (!object || typeof object !== "object") return;

  let newObject = Array.isArray(object) ? [] : {};

  for (let key in object) {if (object.hasOwnProperty(key)) {newObject[key] =
        typeof object[key] === "object" ? deepCopy(object[key]) : object[key];
    }
  }

  return newObject;
}

异步串行 | 异步并行

// 字节面试题,实现一个异步加法
function asyncAdd(a, b, callback) {setTimeout(function () {callback(null, a + b);
  }, 500);
}

// 解决方案
// 1. promisify
const promiseAdd = (a, b) => new Promise((resolve, reject) => {asyncAdd(a, b, (err, res) => {if (err) {reject(err)
    } else {resolve(res)
    }
  })
})

// 2. 串行解决
async function serialSum(...args) {return args.reduce((task, now) => task.then(res => promiseAdd(res, now)), Promise.resolve(0))
}

// 3. 并行处理
async function parallelSum(...args) {if (args.length === 1) return args[0]
  const tasks = []
  for (let i = 0; i < args.length; i += 2) {tasks.push(promiseAdd(args[i], args[i + 1] || 0))
  }
  const results = await Promise.all(tasks)
  return parallelSum(...results)
}

// 测试
(async () => {console.log('Running...');
  const res1 = await serialSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
  console.log(res1)
  const res2 = await parallelSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
  console.log(res2)
  console.log('Done');
})()

实现 apply 办法

思路: 利用 this 的上下文个性。apply其实就是改一下参数的问题

Function.prototype.myApply = function(context = window, args) {
  // 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); // 这里和 call 传参不一样

  // 革除定义的 this 不删除会导致 context 属性越来越多
  delete context[key]; 

  // 返回后果
  return result;
}
// 应用
function f(a,b){console.log(a,b)
 console.log(this.name)
}
let obj={name:'张三'}
f.myApply(obj,[1,2])  //arguments[1]

实现 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

手写类型判断函数

function getType(value) {
  // 判断数据是 null 的状况
  if (value === null) {return value + "";}
  // 判断数据是援用类型的状况
  if (typeof value === "object") {let valueClass = Object.prototype.toString.call(value),
      type = valueClass.split("")[1].split("");
    type.pop();
    return type.join("").toLowerCase();} else {
    // 判断数据是根本数据类型的状况和函数的状况
    return typeof value;
  }
}

实现 Node 的 require 办法

require 基本原理

require 查找门路

requiremodule.exports 干的事件并不简单,咱们先假如有一个全局对象{},初始状况下是空的,当你 require 某个文件时,就将这个文件拿进去执行,如果这个文件外面存在module.exports,当运行到这行代码时将 module.exports 的值退出这个对象,键为对应的文件名,最终这个对象就长这样:

{
  "a.js": "hello world",
  "b.js": function add(){},
  "c.js": 2,
  "d.js": {num: 2}
}

当你再次 require 某个文件时,如果这个对象外面有对应的值,就间接返回给你,如果没有就反复后面的步骤,执行指标文件,而后将它的 module.exports 退出这个全局对象,并返回给调用者。这个全局对象其实就是咱们常常据说的缓存。所以 requiremodule.exports 并没有什么黑魔法,就只是运行并获取指标文件的值,而后退出缓存,用的时候拿进去用就行

手写实现一个 require

const path = require('path'); // 门路操作
const fs = require('fs'); // 文件读取
const vm = require('vm'); // 文件执行

// node 模块化的实现
// node 中是自带模块化机制的,每个文件就是一个独自的模块,并且它遵循的是 CommonJS 标准,也就是应用 require 的形式导入模块,通过 module.export 的形式导出模块。// node 模块的运行机制也很简略,其实就是在每一个模块外层包裹了一层函数,有了函数的包裹就能够实现代码间的作用域隔离

// require 加载模块
// require 依赖 node 中的 fs 模块来加载模块文件,fs.readFile 读取到的是一个字符串。// 在 javascrpt 中咱们能够通过 eval 或者 new Function 的形式来将一个字符串转换成 js 代码来运行。// eval
// const name = 'poetry';
// const str = 'const a = 123; console.log(name)';
// eval(str); // poetry;

// new Function
// new Function 接管的是一个要执行的字符串,返回的是一个新的函数,调用这个新的函数字符串就会执行了。如果这个函数须要传递参数,能够在 new Function 的时候顺次传入参数,最初传入的是要执行的字符串。比方这里传入参数 b,要执行的字符串 str
// const b = 3;
// const str = 'let a = 1; return a + b';
// const fun = new Function('b', str);
// console.log(fun(b, str)); // 4
// 能够看到 eval 和 Function 实例化都能够用来执行 javascript 字符串,仿佛他们都能够来实现 require 模块加载。不过在 node 中并没有选用他们来实现模块化,起因也很简略因为他们都有一个致命的问题,就是都容易被不属于他们的变量所影响。// 如下 str 字符串中并没有定义 a,然而确能够应用下面定义的 a 变量,这显然是不对的,在模块化机制中,str 字符串应该具备本身独立的运行空间,本身不存在的变量是不能够间接应用的
// const a = 1;
// const str = 'console.log(a)';
// eval(str);
// const func = new Function(str);
// func();

// node 存在一个 vm 虚拟环境的概念,用来运行额定的 js 文件,他能够保障 javascript 执行的独立性,不会被内部所影响
// vm 内置模块
// 尽管咱们在内部定义了 hello,然而 str 是一个独立的模块,并不在村 hello 变量,所以会间接报错。// 引入 vm 模块,不须要装置,node 自建模块
// const vm = require('vm');
// const hello = 'poetry';
// const str = 'console.log(hello)';
// wm.runInThisContext(str); // 报错
// 所以 node 执行 javascript 模块时能够采纳 vm 来实现。就能够保障模块的独立性了

// 剖析实现步骤
// 1. 导入相干模块,创立一个 Require 办法。// 2. 抽离通过 Module._load 办法,用于加载模块。// 3.Module.resolveFilename 依据相对路径,转换成绝对路径。// 4. 缓存模块 Module._cache,同一个模块不要反复加载,晋升性能。// 5. 创立模块 id: 保留的内容是 exports = {}相当于 this。// 6. 利用 tryModuleLoad(module, filename) 尝试加载模块。// 7.Module._extensions 应用读取文件。// 8.Module.wrap: 把读取到的 js 包裹一个函数。// 9. 将拿到的字符串应用 runInThisContext 运行字符串。// 10. 让字符串执行并将 this 改编成 exports

// 定义导入类,参数为模块门路
function Require(modulePath) {
    // 获取以后要加载的绝对路径
    let absPathname = path.resolve(__dirname, modulePath);

    // 主动给模块增加后缀名,实现省略后缀名加载模块,其实也就是如果文件没有后缀名的时候遍历一下所有的后缀名看一下文件是否存在
    // 获取所有后缀名
    const extNames = Object.keys(Module._extensions);
    let index = 0;
    // 存储原始文件门路
    const oldPath = absPathname;
    function findExt(absPathname) {if (index === extNames.length) {throw new Error('文件不存在');
        }
        try {fs.accessSync(absPathname);
            return absPathname;
        } catch(e) {const ext = extNames[index++];
            findExt(oldPath + ext);
        }
    }
    // 递归追加后缀名,判断文件是否存在
    absPathname = findExt(absPathname);

    // 从缓存中读取,如果存在,间接返回后果
    if (Module._cache[absPathname]) {return Module._cache[absPathname].exports;
    }

    // 创立模块,新建 Module 实例
    const module = new Module(absPathname);

    // 增加缓存
    Module._cache[absPathname] = module;

    // 加载以后模块
    tryModuleLoad(module);

    // 返回 exports 对象
    return module.exports;
}

// Module 的实现很简略,就是给模块创立一个 exports 对象,tryModuleLoad 执行的时候将内容退出到 exports 中,id 就是模块的绝对路径
// 定义模块, 增加文件 id 标识和 exports 属性
function Module(id) {
    this.id = id;
    // 读取到的文件内容会放在 exports 中
    this.exports = {};}

Module._cache = {};

// 咱们给 Module 挂载动态属性 wrapper,外面定义一下这个函数的字符串,wrapper 是一个数组,数组的第一个元素就是函数的参数局部,其中有 exports,module. Require,__dirname, __filename, 都是咱们模块中罕用的全局变量。留神这里传入的 Require 参数是咱们本人定义的 Require
// 第二个参数就是函数的完结局部。两局部都是字符串,应用的时候咱们将他们包裹在模块的字符串内部就能够了
Module.wrapper = ["(function(exports, module, Require, __dirname, __filename) {",
    "})"
]

// _extensions 用于针对不同的模块扩展名应用不同的加载形式,比方 JSON 和 javascript 加载形式必定是不同的。JSON 应用 JSON.parse 来运行。// javascript 应用 vm.runInThisContext 来运行,能够看到 fs.readFileSync 传入的是 module.id 也就是咱们 Module 定义时候 id 存储的是模块的绝对路径,读取到的 content 是一个字符串,咱们应用 Module.wrapper 来包裹一下就相当于在这个模块内部又包裹了一个函数,也就实现了公有作用域。// 应用 call 来执行 fn 函数,第一个参数扭转运行的 this 咱们传入 module.exports,前面的参数就是函数里面包裹参数 exports, module, Require, __dirname, __filename
Module._extensions = {'.js'(module) {const content = fs.readFileSync(module.id, 'utf8');
        const fnStr = Module.wrapper[0] + content + Module.wrapper[1];
        const fn = vm.runInThisContext(fnStr);
        fn.call(module.exports, module.exports, module, Require,__filename,__dirname);
    },
    '.json'(module) {const json = fs.readFileSync(module.id, 'utf8');
        module.exports = JSON.parse(json); // 把文件的后果放在 exports 属性上
    }
}

// tryModuleLoad 函数接管的是模块对象,通过 path.extname 来获取模块的后缀名,而后应用 Module._extensions 来加载模块
// 定义模块加载办法
function tryModuleLoad(module) {
    // 获取扩展名
    const extension = path.extname(module.id);
    // 通过后缀加载以后模块
    Module._extensions[extension](module);
}

// 至此 Require 加载机制咱们根本就写完了,咱们来从新看一下。Require 加载模块的时候传入模块名称,在 Require 办法中应用 path.resolve(__dirname, modulePath)获取到文件的绝对路径。而后通过 new Module 实例化的形式创立 module 对象,将模块的绝对路径存储在 module 的 id 属性中,在 module 中创立 exports 属性为一个 json 对象
// 应用 tryModuleLoad 办法去加载模块,tryModuleLoad 中应用 path.extname 获取到文件的扩展名,而后依据扩展名来执行对应的模块加载机制
// 最终将加载到的模块挂载 module.exports 中。tryModuleLoad 执行结束之后 module.exports 曾经存在了,间接返回就能够了


// 给模块增加缓存
// 增加缓存也比较简单,就是文件加载的时候将文件放入缓存中,再去加载模块时先看缓存中是否存在,如果存在间接应用,如果不存在再去从新,加载之后再放入缓存

// 测试
let json = Require('./test.json');
let test2 = Require('./test2.js');
console.log(json);
console.log(test2);

异步并发数限度

/**
 * 关键点
 * 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)
})

实现一个迭代器生成函数

ES6 对迭代器的实现

JS 原生的汇合类型数据结构,只有 Array(数组)和Object(对象);而ES6 中,又新增了 MapSet。四种数据结构各自有着本人特地的外部实现,但咱们仍期待以同样的一套规定去遍历它们,所以 ES6 在推出新数据结构的同时也推出了一套 对立的接口机制 ——迭代器(Iterator)。

ES6约定,任何数据结构只有具备 Symbol.iterator 属性(这个属性就是 Iterator 的具体实现,它实质上是以后数据结构默认的迭代器生成函数),就能够被遍历——精确地说,是被 for...of... 循环和迭代器的 next 办法遍历。事实上,for...of...的背地正是对 next 办法的重复调用。

在 ES6 中,针对 ArrayMapSetStringTypedArray、函数的 arguments 对象、NodeList 对象这些原生的数据结构都能够通过for...of... 进行遍历。原理都是一样的,此处咱们拿最简略的数组进行举例,当咱们用 for...of... 遍历数组时:

const arr = [1, 2, 3]
const len = arr.length
for(item of arr) {console.log(` 以后元素是 ${item}`)
}

之所以可能按程序一次一次地拿到数组里的每一个成员,是因为咱们借助数组的 Symbol.iterator 生成了它对应的迭代器对象,通过重复调用迭代器对象的 next 办法拜访了数组成员,像这样:

const arr = [1, 2, 3]
// 通过调用 iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()

// 对迭代器对象执行 next,就能一一拜访汇合的成员
iterator.next()
iterator.next()
iterator.next()

丢进控制台,咱们能够看到 next 每次会按程序帮咱们拜访一个汇合成员:

for...of... 做的事件,根本等价于上面这通操作:

// 通过调用 iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()

// 初始化一个迭代后果
let now = {done: false}

// 循环往外迭代成员
while(!now.done) {now = iterator.next()
    if(!now.done) {console.log(` 当初遍历到了 ${now.value}`)
    }
}

能够看出,for...of...其实就是 iterator 循环调用换了种写法。在 ES6 中咱们之所以可能开心地用 for...of... 遍历各种各种的汇合,全靠迭代器模式在背地给力。

ps:此处举荐浏览迭代协定 (opens new window),置信大家读过后会对迭代器在 ES6 中的实现有更深的了解。

退出移动版