关于前端:这些js手写题对我这个菜鸟来说写不出来

4次阅读

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

实现每隔一秒打印 1,2,3,4

// 应用闭包实现
for (var i = 0; i < 5; i++) {(function(i) {setTimeout(function() {console.log(i);
    }, i * 1000);
  })(i);
}
// 应用 let 块级作用域
for (let i = 0; i < 5; i++) {setTimeout(function() {console.log(i);
  }, i * 1000);
}

手写 apply 函数

apply 函数的实现步骤:

  1. 判断调用对象是否为函数,即便咱们是定义在函数的原型上的,然而可能呈现应用 call 等形式调用的状况。
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window。
  3. 将函数作为上下文对象的一个属性。
  4. 判断参数值是否传入
  5. 应用上下文对象来调用这个办法,并保留返回后果。
  6. 删除方才新增的属性
  7. 返回后果
// apply 函数实现
Function.prototype.myApply = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {throw new TypeError("Error");
  }
  let result = null;
  // 判断 context 是否存在,如果未传入则为 window
  context = context || window;
  // 将函数设为对象的办法
  context.fn = this;
  // 调用办法
  if (arguments[1]) {result = context.fn(...arguments[1]);
  } else {result = context.fn();
  }
  // 将属性删除
  delete context.fn;
  return result;
};

前端手写面试题具体解答

实现防抖函数(debounce)

防抖函数原理:在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则从新计时。

那么与节流函数的区别间接看这个动画实现即可。

手写简化版:

// 防抖函数
const debounce = (fn, delay) => {
  let timer = null;
  return (...args) => {clearTimeout(timer);
    timer = setTimeout(() => {fn.apply(this, args);
    }, delay);
  };
};

实用场景:

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

生存环境请用 lodash.debounce

实现数组的乱序输入

次要的实现思路就是:

  • 取出数组的第一个元素,随机产生一个索引值,将该第一个元素和这个索引对应的元素进行替换。
  • 第二次取出数据数组第二个元素,随机产生一个除了索引为 1 的之外的索引值,并将第二个元素与该索引值对应的元素进行替换
  • 依照下面的法则执行,直到遍历实现
var arr = [1,2,3,4,5,6,7,8,9,10];
for (var i = 0; i < arr.length; i++) {const randomIndex = Math.round(Math.random() * (arr.length - 1 - i)) + i;
  [arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];
}
console.log(arr)

还有一办法就是倒序遍历:

var arr = [1,2,3,4,5,6,7,8,9,10];
let length = arr.length,
    randomIndex,
    temp;
  while (length) {randomIndex = Math.floor(Math.random() * length--);
    temp = arr[length];
    arr[length] = arr[randomIndex];
    arr[randomIndex] = temp;
  }
console.log(arr)

手写 Object.create

思路:将传入的对象作为原型

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

手写类型判断函数

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

实现数组的 flat 办法

function _flat(arr, depth) {if(!Array.isArray(arr) || depth <= 0) {return arr;}
  return arr.reduce((prev, cur) => {if (Array.isArray(cur)) {return prev.concat(_flat(cur, depth - 1))
    } else {return prev.concat(cur);
    }
  }, []);
}

实现深拷贝

简洁版本

简略版:

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

实现一个双向绑定

defineProperty 版本

// 数据
const data = {text: 'default'};
const input = document.getElementById('input');
const span = document.getElementById('span');
// 数据劫持
Object.defineProperty(data, 'text', {
  // 数据变动 --> 批改视图
  set(newVal) {
    input.value = newVal;
    span.innerHTML = newVal;
  }
});
// 视图更改 --> 数据变动
input.addEventListener('keyup', function(e) {data.text = e.target.value;});

proxy 版本

// 数据
const data = {text: 'default'};
const input = document.getElementById('input');
const span = document.getElementById('span');
// 数据劫持
const handler = {set(target, key, value) {target[key] = value;
    // 数据变动 --> 批改视图
    input.value = value;
    span.innerHTML = value;
    return value;
  }
};
const proxy = new Proxy(data, handler);

// 视图更改 --> 数据变动
input.addEventListener('keyup', function(e) {proxy.text = e.target.value;});

数组去重办法汇总

首先: 我晓得多少种去重形式

1. 双层 for 循环

function distinct(arr) {for (let i=0, len=arr.length; i<len; i++) {for (let j=i+1; j<len; j++) {if (arr[i] == arr[j]) {arr.splice(j, 1);
                // splice 会扭转数组长度,所以要将数组长度 len 和下标 j 减一
                len--;
                j--;
            }
        }
    }
    return arr;
}

思维: 双重 for 循环是比拟蠢笨的办法,它实现的原理很简略:先定义一个蕴含原始数组第一个元素的数组,而后遍历原始数组,将原始数组中的每个元素与新数组中的每个元素进行比对,如果不反复则增加到新数组中,最初返回新数组;因为它的工夫复杂度是O(n^2),如果数组长度很大,效率会很低

2. Array.filter() 加 indexOf/includes

function distinct(a, b) {let arr = a.concat(b);
    return arr.filter((item, index)=> {//return arr.indexOf(item) === index
        return arr.includes(item)
    })
}

思维: 利用 indexOf 检测元素在数组中第一次呈现的地位是否和元素当初的地位相等,如果不等则阐明该元素是反复元素

3. ES6 中的 Set 去重

function distinct(array) {return Array.from(new Set(array));
}

思维: ES6 提供了新的数据结构 Set,Set 构造的一个个性就是成员值都是惟一的,没有反复的值。

4. reduce 实现对象数组去反复

var resources = [{ name: "张三", age: "18"},
    {name: "张三", age: "19"},
    {name: "张三", age: "20"},
    {name: "李四", age: "19"},
    {name: "王五", age: "20"},
    {name: "赵六", age: "21"}
]
var temp = {};
resources = resources.reduce((prev, curv) => {
 // 如果长期对象中有这个名字,什么都不做
 if (temp[curv.name]) { }else {
    // 如果长期对象没有就把这个名字加进去,同时把以后的这个对象退出到 prev 中
    temp[curv.name] = true;
    prev.push(curv);
 }
 return prev
}, []);
console.log("后果", resources);

这种办法是利用高阶函数 reduce 进行去重,这里只须要留神 initialValue 得放一个空数组[],不然没法push

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

实现 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);
}

字符串查找

请应用最根本的遍从来实现判断字符串 a 是否被蕴含在字符串 b 中,并返回第一次呈现的地位(找不到返回 -1)。

a='34';b='1234567'; // 返回 2
a='35';b='1234567'; // 返回 -1
a='355';b='12354355'; // 返回 5
isContain(a,b);

function isContain(a, b) {for (let i in b) {if (a[0] === b[i]) {
      let tmp = true;
      for (let j in a) {if (a[j] !== b[~~i + ~~j]) {tmp = false;}
      }
      if (tmp) {return i;}
    }
  }
  return -1;
}

实现一个迷你版的 vue

入口

// js/vue.js
class Vue {constructor (options) {
    // 1. 通过属性保留选项的数据
    this.$options = options || {}
    this.$data = options.data || {}
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
    // 2. 把 data 中的成员转换成 getter 和 setter,注入到 vue 实例中
    this._proxyData(this.$data)
    // 3. 调用 observer 对象,监听数据的变动
    new Observer(this.$data)
    // 4. 调用 compiler 对象,解析指令和差值表达式
    new Compiler(this)
  }
  _proxyData (data) {
    // 遍历 data 中的所有属性
    Object.keys(data).forEach(key => {
      // 把 data 的属性注入到 vue 实例中
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get () {return data[key]
        },
        set (newValue) {if (newValue === data[key]) {return}
          data[key] = newValue
        }
      })
    })
  }
}

实现 Dep

class Dep {constructor () {
    // 存储所有的观察者
    this.subs = []}
  // 增加观察者
  addSub (sub) {if (sub && sub.update) {this.subs.push(sub)
    }
  }
  // 发送告诉
  notify () {
    this.subs.forEach(sub => {sub.update()
    })
  }
}

实现 watcher

class Watcher {constructor (vm, key, cb) {
    this.vm = vm
    // data 中的属性名称
    this.key = key
    // 回调函数负责更新视图
    this.cb = cb

    // 把 watcher 对象记录到 Dep 类的动态属性 target
    Dep.target = this
    // 触发 get 办法,在 get 办法中会调用 addSub
    this.oldValue = vm[key]
    Dep.target = null
  }
  // 当数据发生变化的时候更新视图
  update () {let newValue = this.vm[this.key]
    if (this.oldValue === newValue) {return}
    this.cb(newValue)
  }
}

实现 compiler

class Compiler {constructor (vm) {
    this.el = vm.$el
    this.vm = vm
    this.compile(this.el)
  }
  // 编译模板,解决文本节点和元素节点
  compile (el) {
    let childNodes = el.childNodes
    Array.from(childNodes).forEach(node => {
      // 解决文本节点
      if (this.isTextNode(node)) {this.compileText(node)
      } else if (this.isElementNode(node)) {
        // 解决元素节点
        this.compileElement(node)
      }

      // 判断 node 节点,是否有子节点,如果有子节点,要递归调用 compile
      if (node.childNodes && node.childNodes.length) {this.compile(node)
      }
    })
  }
  // 编译元素节点,解决指令
  compileElement (node) {// console.log(node.attributes)
    // 遍历所有的属性节点
    Array.from(node.attributes).forEach(attr => {
      // 判断是否是指令
      let attrName = attr.name
      if (this.isDirective(attrName)) {
        // v-text --> text
        attrName = attrName.substr(2)
        let key = attr.value
        this.update(node, key, attrName)
      }
    })
  }

  update (node, key, attrName) {let updateFn = this[attrName + 'Updater']
    updateFn && updateFn.call(this, node, this.vm[key], key)
  }

  // 解决 v-text 指令
  textUpdater (node, value, key) {
    node.textContent = value
    new Watcher(this.vm, key, (newValue) => {node.textContent = newValue})
  }
  // v-model
  modelUpdater (node, value, key) {
    node.value = value
    new Watcher(this.vm, key, (newValue) => {node.value = newValue})
    // 双向绑定
    node.addEventListener('input', () => {this.vm[key] = node.value
    })
  }

  // 编译文本节点,解决差值表达式
  compileText (node) {// console.dir(node)
    // {{msg}}
    let reg = /\{\{(.+?)\}\}/
    let value = node.textContent
    if (reg.test(value)) {let key = RegExp.$1.trim()
      node.textContent = value.replace(reg, this.vm[key])

      // 创立 watcher 对象,当数据扭转更新视图
      new Watcher(this.vm, key, (newValue) => {node.textContent = newValue})
    }
  }
  // 判断元素属性是否是指令
  isDirective (attrName) {return attrName.startsWith('v-')
  }
  // 判断节点是否是文本节点
  isTextNode (node) {return node.nodeType === 3}
  // 判断节点是否是元素节点
  isElementNode (node) {return node.nodeType === 1}
}

实现 Observer

class Observer {constructor (data) {this.walk(data)
  }
  walk (data) {
    // 1. 判断 data 是否是对象
    if (!data || typeof data !== 'object') {return}
    // 2. 遍历 data 对象的所有属性
    Object.keys(data).forEach(key => {this.defineReactive(data, key, data[key])
    })
  }
  defineReactive (obj, key, val) {
    let that = this
    // 负责收集依赖,并发送告诉
    let dep = new Dep()
    // 如果 val 是对象,把 val 外部的属性转换成响应式数据
    this.walk(val)
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get () {
        // 收集依赖
        Dep.target && dep.addSub(Dep.target)
        return val
      },
      set (newValue) {if (newValue === val) {return}
        val = newValue
        that.walk(newValue)
        // 发送告诉
        dep.notify()}
    })
  }
}

应用

<!DOCTYPE html>
<html lang="cn">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Mini Vue</title>
</head>
<body>
  <div id="app">
    <h1> 差值表达式 </h1>
    <h3>{{msg}}</h3>
    <h3>{{count}}</h3>
    <h1>v-text</h1>
    <div v-text="msg"></div>
    <h1>v-model</h1>
    <input type="text" v-model="msg">
    <input type="text" v-model="count">
  </div>
  <script src="./js/dep.js"></script>
  <script src="./js/watcher.js"></script>
  <script src="./js/compiler.js"></script>
  <script src="./js/observer.js"></script>
  <script src="./js/vue.js"></script>
  <script>
    let vm = new Vue({
      el: '#app',
      data: {
        msg: 'Hello Vue',
        count: 100,
        person: {name: 'zs'}
      }
    })
    console.log(vm.msg)
    // vm.msg = {test: 'Hello'}
    vm.test = 'abc'
  </script>
</body>
</html>

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

实现 ES6 的 const

因为 ES5 环境没有 block 的概念,所以是无奈百分百实现 const,只能是挂载到某个对象下,要么是全局的window,要么就是自定义一个object 来当容器

var __const = function __const (data, value) {
    window.data = value // 把要定义的 data 挂载到 window 下,并赋值 value
    Object.defineProperty(window, data, { // 利用 Object.defineProperty 的能力劫持以后对象,并批改其属性描述符
      enumerable: false,
      configurable: false,
      get: function () {return value},
      set: function (data) {if (data !== value) { // 当要对以后属性进行赋值时,则抛出谬误!throw new TypeError('Assignment to constant variable.')
        } else {return value}
      }
    })
  }
  __const('a', 10)
  console.log(a)
  delete a
  console.log(a)
  for (let item in window) { // 因为 const 定义的属性在 global 下也是不存在的,所以用到了 enumerable: false 来模仿这一性能
    if (item === 'a') { // 因为不可枚举,所以不执行
      console.log(window[item])
    }
  }
  a = 20 // 报错

Vue目前双向绑定的外围实现思路就是利用 Object.definePropertygetset 进行劫持,监听用户对属性进行调用以及赋值时的具体情况,从而实现的双向绑定

实现 findIndex 办法

var users = [{id: 1, name: '张三'},
  {id: 2, name: '张三'},
  {id: 3, name: '张三'},
  {id: 4, name: '张三'}
]

Array.prototype.myFindIndex = function (callback) {// var callback = function (item, index) {return item.id === 4}
  for (var i = 0; i < this.length; i++) {if (callback(this[i], i)) {
      // 这里返回
      return i
    }
  }
}

var ret = users.myFind(function (item, index) {return item.id === 2})

console.log(ret)

实现事件总线联合 Vue 利用

Event Bus(Vue、Flutter 等前端框架中有出镜)和 Event Emitter(Node 中有出镜)出场的“剧组”不同,然而它们都对应一个独特的角色—— 全局事件总线

全局事件总线,严格来说不能说是观察者模式,而是公布 - 订阅模式。它在咱们日常的业务开发中利用十分广。

如果只能选一道题,那这道题肯定是 Event Bus/Event Emitter 的代码实现——我都说这么分明了,这个知识点到底要不要把握、须要把握到什么水平,就看各位本人的了。

在 Vue 中应用 Event Bus 来实现组件间的通信

Event Bus/Event Emitter 作为全局事件总线,它起到的是一个 沟通桥梁 的作用。咱们能够把它了解为一个事件核心,咱们所有事件的订阅 / 公布都不能由订阅方和公布方“私下沟通”,必须要委托这个事件核心帮咱们实现。

在 Vue 中,有时候 A 组件和 B 组件中距离了很远,看似没什么关系,但咱们心愿它们之间可能通信。这种状况下除了求助于 Vuex 之外,咱们还能够通过 Event Bus 来实现咱们的需要。

创立一个 Event Bus(实质上也是 Vue 实例)并导出:

const EventBus = new Vue()
export default EventBus

在主文件里引入EventBus,并挂载到全局:

import bus from 'EventBus 的文件门路'
Vue.prototype.bus = bus

订阅事件:

// 这里 func 指 someEvent 这个事件的监听函数
this.bus.$on('someEvent', func)

公布(触发)事件:

// 这里 params 指 someEvent 这个事件被触发时回调函数接管的入参
this.bus.$emit('someEvent', params)

大家会发现,整个调用过程中,没有呈现具体的发布者和订阅者(比方下面的 PrdPublisherDeveloperObserver),全程只有 bus 这个货色一个人在疯狂刷存在感。这就是全局事件总线的特点——所有事件的公布 / 订阅操作,必须经由事件核心,禁止所有“私下交易”!

上面,咱们就一起来实现一个Event Bus(留神看正文里的解析):

class EventEmitter {constructor() {
    // handlers 是一个 map,用于存储事件与回调之间的对应关系
    this.handlers = {}}

  // on 办法用于装置事件监听器,它承受指标事件名和回调函数作为参数
  on(eventName, cb) {
    // 先检查一下指标事件名有没有对应的监听函数队列
    if (!this.handlers[eventName]) {
      // 如果没有,那么首先初始化一个监听函数队列
      this.handlers[eventName] = []}

    // 把回调函数推入指标事件的监听函数队列里去
    this.handlers[eventName].push(cb)
  }

  // emit 办法用于触发指标事件,它承受事件名和监听函数入参作为参数
  emit(eventName, ...args) {
    // 查看指标事件是否有监听函数队列
    if (this.handlers[eventName]) {
      // 如果有,则一一调用队列里的回调函数
      this.handlers[eventName].forEach((callback) => {callback(...args)
      })
    }
  }

  // 移除某个事件回调队列里的指定回调函数
  off(eventName, cb) {const callbacks = this.handlers[eventName]
    const index = callbacks.indexOf(cb)
    if (index !== -1) {callbacks.splice(index, 1)
    }
  }

  // 为事件注册单次监听器
  once(eventName, cb) {
    // 对回调函数进行包装,使其执行结束主动被移除
    const wrapper = (...args) => {cb.apply(...args)
      this.off(eventName, wrapper)
    }
    this.on(eventName, wrapper)
  }
}

在日常的开发中,大家用到 EventBus/EventEmitter 往往提供比这五个办法多的多的多的办法。但在面试过程中,如果大家可能残缺地实现出这五个办法,曾经十分能够阐明问题了,因而楼上这个 EventBus 心愿大家能够熟练掌握。学有余力的同学

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

查找文章中呈现频率最高的单词

function findMostWord(article) {
  // 合法性判断
  if (!article) return;
  // 参数解决
  article = article.trim().toLowerCase();
  let wordList = article.match(/[a-z]+/g),
    visited = [],
    maxNum = 0,
    maxWord = "";
  article = "" + wordList.join("  ") +" ";
  // 遍历判断单词呈现次数
  wordList.forEach(function(item) {if (visited.indexOf(item) < 0) {
      // 退出 visited 
      visited.push(item);
      let word = new RegExp("" + item +" ","g"),
        num = article.match(word).length;
      if (num > maxNum) {
        maxNum = num;
        maxWord = item;
      }
    }
  });
  return maxWord + " " + maxNum;
}
正文完
 0