实现每隔一秒打印 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 函数的实现步骤:
- 判断调用对象是否为函数,即便咱们是定义在函数的原型上的,然而可能呈现应用 call 等形式调用的状况。
- 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
- 将函数作为上下文对象的一个属性。
- 判断参数值是否传入
- 应用上下文对象来调用这个办法,并保留返回后果。
- 删除方才新增的属性
- 返回后果
// 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等非凡对象的克隆
- 会摈弃对象的
constructo
r,所有的构造函数会指向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}
// testvar o = {};o.x = o;var o1 = deepClone(o); // 如果这个对象拷贝过了 就返回那个拷贝的后果就能够了console.log(o1);
实现残缺的深拷贝
1. 简易版及问题
JSON.parse(JSON.stringify());
预计这个api能笼罩大多数的利用场景,没错,谈到深拷贝,我第一个想到的也是它。然而实际上,对于某些严格的场景来说,这个办法是有微小的坑的。问题如下:
- 无奈解决
循环援用
的问题。举个例子:
const a = {val:2};a.target = a;
拷贝a
会呈现零碎栈溢出,因为呈现了有限递归的状况。
- 无奈拷贝一些非凡的对象,诸如
RegExp, Date, Set, Map
等 - 无奈拷贝
函数
(划重点)。
因而这个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'; // 返回 2a='35';b='1234567'; // 返回 -1a='355';b='12354355'; // 返回 5isContain(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.jsclass 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 查找门路
require
和module.exports
干的事件并不简单,咱们先假如有一个全局对象{}
,初始状况下是空的,当你require
某个文件时,就将这个文件拿进去执行,如果这个文件外面存在module.exports
,当运行到这行代码时将module.exports
的值退出这个对象,键为对应的文件名,最终这个对象就长这样:
{ "a.js": "hello world", "b.js": function add(){}, "c.js": 2, "d.js": { num: 2 }}
当你再次require
某个文件时,如果这个对象外面有对应的值,就间接返回给你,如果没有就反复后面的步骤,执行指标文件,而后将它的module.exports
退出这个全局对象,并返回给调用者。这个全局对象其实就是咱们常常据说的缓存。所以require
和module.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, __filenameModule._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
,只能是挂载到某个对象下,要么是全局的windo
w,要么就是自定义一个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.defineProperty
对get
跟set
进行劫持,监听用户对属性进行调用以及赋值时的具体情况,从而实现的双向绑定
实现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)
大家会发现,整个调用过程中,没有呈现具体的发布者和订阅者(比方下面的PrdPublisher
和DeveloperObserver
),全程只有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;}