石家庄网站建设公司logo设计大师
实现一个compose函数
组合多个函数,从右到左,比如:
compose(f, g, h)
最终得到这个结果(...args) => f(g(h(...args))).
题目描述:实现一个 compose
函数
// 用法如下:
function fn1(x) {return x + 1;
}
function fn2(x) {return x + 2;
}
function fn3(x) {return x + 3;
}
function fn4(x) {return x + 4;
}
const a = compose(fn1, fn2, fn3, fn4);
console.log(a(1)); // 1+4+3+2+1=11
实现代码如下
function compose(...funcs) {if (!funcs.length) return (v) => v;if (funcs.length === 1) {return funcs[0]}return funcs.reduce((a, b) => {return (...args) => a(b(...args)))}
}
compose
创建了一个从右向左执行的数据流。如果要实现从左到右的数据流,可以直接更改compose
的部分代码即可实现
- 更换
Api
接口:把reduce
改为reduceRight
- 交互包裹位置:把
a(b(...args))
改为b(a(...args))
实现迭代器生成函数
我们说迭代器对象全凭迭代器生成函数帮我们生成。在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.lengthreturn {// 自定义next方法next: function() {// 如果索引还没有超出集合长度,done为falsevar 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()
此处为了记录每次遍历的位置,我们实现了一个闭包,借助自由变量来做我们的迭代过程中的“游标”。
运行一下我们自定义的迭代器,结果符合预期:
实现单例模式
核心要点: 用闭包和
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);
}
实现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;
}
实现redux中间件
简单实现
function createStore(reducer) {let currentStatelet listeners = []function getState() {return currentState}function dispatch(action) {currentState = reducer(currentState, action)listeners.map(listener => {listener()})return action}function subscribe(cb) {listeners.push(cb)return () => {}}dispatch({type: 'ZZZZZZZZZZ'})return {getState,dispatch,subscribe}
}// 应用实例如下:
function reducer(state = 0, action) {switch (action.type) {case 'ADD':return state + 1case 'MINUS':return state - 1default:return state}
}const store = createStore(reducer)console.log(store);
store.subscribe(() => {console.log('change');
})
console.log(store.getState());
console.log(store.dispatch({type: 'ADD'}));
console.log(store.getState());
2. 迷你版
export const createStore = (reducer,enhancer)=>{if(enhancer) {return enhancer(createStore)(reducer)}let currentState = {}let currentListeners = []const getState = ()=>currentStateconst subscribe = (listener)=>{currentListeners.push(listener)}const dispatch = action=>{currentState = reducer(currentState, action)currentListeners.forEach(v=>v())return action}dispatch({type:'@@INIT'})return {getState,subscribe,dispatch}
}//中间件实现
export applyMiddleWare(...middlewares){return createStore=>...args=>{const store = createStore(...args)let dispatch = store.dispatchconst midApi = {getState:store.getState,dispatch:...args=>dispatch(...args)}const middlewaresChain = middlewares.map(middleware=>middleware(midApi))dispatch = compose(...middlewaresChain)(store.dispatch)return {...store,dispatch}}// fn1(fn2(fn3())) 把函数嵌套依次调用
export function compose(...funcs){if(funcs.length===0){return arg=>arg}if(funs.length===1){return funs[0]}return funcs.reduce((ret,item)=>(...args)=>ret(item(...args)))
}//bindActionCreator实现function bindActionCreator(creator,dispatch){return ...args=>dispatch(creator(...args))
}
function bindActionCreators(creators,didpatch){//let bound = {}//Object.keys(creators).forEach(v=>{// let creator = creator[v]// bound[v] = bindActionCreator(creator,dispatch)//})//return boundreturn Object.keys(creators).reduce((ret,item)=>{ret[item] = bindActionCreator(creators[item],dispatch)return ret},{})
}
实现数组扁平化flat方法
题目描述: 实现一个方法使多维数组变成一维数组
let ary = [1, [2, [3, [4, 5]]], 6];
let str = JSON.stringify(ary);
第0种处理:直接的调用
arr_flat = arr.flat(Infinity);
第一种处理
ary = str.replace(/(\[|\])/g, '').split(',');
第二种处理
str = str.replace(/(\[\]))/g, '');
str = '[' + str + ']';
ary = JSON.parse(str);
第三种处理:递归处理
let result = [];
let fn = function(ary) {for(let i = 0; i < ary.length; i++) }{let item = ary[i];if (Array.isArray(ary[i])){fn(item);} else {result.push(item);}}
}
第四种处理:用 reduce 实现数组的 flat 方法
function flatten(ary) {return ary.reduce((pre, cur) => {return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);}, []);
}
let ary = [1, 2, [3, 4], [5, [6, 7]]]
console.log(flatten(ary))
第五种处理:能用迭代的思路去实现
function flatten(arr) {if (!arr.length) return;while (arr.some((item) => Array.isArray(item))) {arr = [].concat(...arr);}return arr;
}
// console.log(flatten([1, 2, [1, [2, 3, [4, 5, [6]]]]]));
第六种处理:扩展运算符
while (ary.some(Array.isArray)) {ary = [].concat(...ary);
}
参考 前端进阶面试题详细解答
实现深拷贝
简洁版本
简单版:
const newObj = JSON.parse(JSON.stringify(oldObj));
局限性:
- 他无法实现对函数 、RegExp等特殊对象的克隆
- 会抛弃对象的
constructo
r,所有的构造函数会指向Object
- 对象有循环引用,会报错
面试简版
function deepClone(obj) {// 如果是 值类型 或 null,则直接returnif(typeof obj !== 'object' || obj === null) {return obj}// 定义结果对象let copy = {}// 如果对象是数组,则定义结果数组if(obj.constructor === Array) {copy = []}// 遍历对象的keyfor(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) {//处理Maptarget.forEach((item, key) => {cloneTarget.set(deepClone(key), deepClone(item));})}if(type === setTag) {//处理Settarget.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) {//处理Maptarget.forEach((item, key) => {cloneTarget.set(deepClone(key, map), deepClone(item, map));})}if(type === setTag) {//处理Settarget.forEach(item => {cloneTarget.add(deepClone(item, map));})}// 处理数组和对象for (let prop in target) {if (target.hasOwnProperty(prop)) {cloneTarget[prop] = deepClone(target[prop], map);}}return cloneTarget;
}
实现every方法
Array.prototype.myEvery=function(callback, context = window){var len=this.length,flag=true,i = 0;for(;i < len; i++){if(!callback.apply(context,[this[i], i , this])){flag=false;break;} }return flag;}// var obj = {num: 1}// var aa=arr.myEvery(function(v,index,arr){// return v.num>=12;// },obj)// console.log(aa)
实现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;
}
实现一个迷你版的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类的静态属性targetDep.target = this// 触发get方法,在get方法中会调用addSubthis.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.$elthis.vm = vmthis.compile(this.el)}// 编译模板,处理文本节点和元素节点compile (el) {let childNodes = el.childNodesArray.from(childNodes).forEach(node => {// 处理文本节点if (this.isTextNode(node)) {this.compileText(node)} else if (this.isElementNode(node)) {// 处理元素节点this.compileElement(node)}// 判断node节点,是否有子节点,如果有子节点,要递归调用compileif (node.childNodes && node.childNodes.length) {this.compile(node)}})}// 编译元素节点,处理指令compileElement (node) {// console.log(node.attributes)// 遍历所有的属性节点Array.from(node.attributes).forEach(attr => {// 判断是否是指令let attrName = attr.nameif (this.isDirective(attrName)) {// v-text --> textattrName = attrName.substr(2)let key = attr.valuethis.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 = valuenew Watcher(this.vm, key, (newValue) => {node.textContent = newValue})}// v-modelmodelUpdater (node, value, key) {node.value = valuenew 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.textContentif (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 = newValuethat.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>
实现ES6的extends
function B(name){this.name = name;
};
function A(name,age){//1.将A的原型指向BObject.setPrototypeOf(A,B);//2.用A的实例作为this调用B,得到继承B之后的实例,这一步相当于调用superObject.getPrototypeOf(A).call(this, name)//3.将A原有的属性添加到新实例上this.age = age; //4.返回新实例对象return this;
};
var a = new A('poetry',22);
console.log(a);
实现ES6的const
由于ES5环境没有
block
的概念,所以是无法百分百实现const
,只能是挂载到某个对象下,要么是全局的windo
w,要么就是自定义一个object
来当容器
var __const = function __const (data, value) {window.data = value // 把要定义的data挂载到window下,并赋值valueObject.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 aconsole.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
进行劫持,监听用户对属性进行调用以及赋值时的具体情况,从而实现的双向绑定
实现节流函数(throttle)
节流函数原理:指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的时间才会执行回调函数。总结起来就是: 事件,按照一段时间的间隔来进行触发 。
像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多
手写简版
使用时间戳的节流函数会在第一次触发事件时立即执行,以后每过 wait 秒之后才执行一次,并且最后一次触发事件不会被执行
时间戳方式:
// func是用户传入需要防抖的函数
// wait是等待时间
const throttle = (func, wait = 50) => {// 上一次执行该函数的时间let lastTime = 0return function(...args) {// 当前时间let now = +new Date()// 将当前时间和上一次执行函数时间对比// 如果差值大于设置的等待时间就执行函数if (now - lastTime > wait) {lastTime = nowfunc.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时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
- 函数节流 :使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。
实现模板字符串解析功能
let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {name: '姓名',age: 18
}
render(template, data); // 我是姓名,年龄18,性别undefined
function render(template, data) {const reg = /\{\{(\w+)\}\}/; // 模板字符串正则if (reg.test(template)) { // 判断模板里是否有模板字符串const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段template = template.replace(reg, data[name]); // 将第一个模板字符串渲染return render(template, data); // 递归的渲染并返回渲染后的结构}return template; // 如果模板没有模板字符串直接返回
}
实现一个简易的MVVM
实现一个简易的
MVVM
我会分为这么几步来:
- 首先我会定义一个类
Vue
,这个类接收的是一个options
,那么其中可能有需要挂载的根元素的id
,也就是el
属性;然后应该还有一个data
属性,表示需要双向绑定的数据 - 其次我会定义一个
Dep
类,这个类产生的实例对象中会定义一个subs
数组用来存放所依赖这个属性的依赖,已经添加依赖的方法addSub
,删除方法removeSub
,还有一个notify
方法用来遍历更新它subs
中的所有依赖,同时Dep类有一个静态属性target
它用来表示当前的观察者,当后续进行依赖收集的时候可以将它添加到dep.subs
中。 - 然后设计一个
observe
方法,这个方法接收的是传进来的data
,也就是options.data
,里面会遍历data
中的每一个属性,并使用Object.defineProperty()
来重写它的get
和set
,那么这里面呢可以使用new Dep()
实例化一个dep
对象,在get
的时候调用其addSub
方法添加当前的观察者Dep.target
完成依赖收集,并且在set
的时候调用dep.notify
方法来通知每一个依赖它的观察者进行更新 - 完成这些之后,我们还需要一个
compile
方法来将HTML模版和数据结合起来。在这个方法中首先传入的是一个node
节点,然后遍历它的所有子级,判断是否有firstElmentChild
,有的话则进行递归调用compile方法,没有firstElementChild
的话且该child.innderHTML
用正则匹配满足有/\{\{(.*)\}\}/
项的话则表示有需要双向绑定的数据,那么就将用正则new Reg('\\{\\{\\s*' + key + '\\s*\\}\\}', 'gm')
替换掉是其为msg
变量。 - 完成变量替换的同时,还需要将
Dep.target
指向当前的这个child
,且调用一下this.opt.data[key]
,也就是为了触发这个数据的get
来对当前的child
进行依赖收集,这样下次数据变化的时候就能通知child
进行视图更新了,不过在最后要记得将Dep.target
指为null
哦(其实在Vue
中是有一个targetStack
栈用来存放target
的指向的) - 那么最后我们只需要监听
document
的DOMContentLoaded
然后在回调函数中实例化这个Vue
对象就可以了
coding :
需要注意的点:
childNodes
会获取到所有的子节点以及文本节点(包括元素标签中的空白节点)firstElementChild
表示获取元素的第一个字元素节点,以此来区分是不是元素节点,如果是的话则调用compile
进行递归调用,否则用正则匹配- 这里面的正则真的不难,大家可以看一下
完整代码如下:
<!DOCTYPE html>
<html lang="en"><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>MVVM</title></head><body><div id="app"><h3>姓名</h3><p>{{name}}</p><h3>年龄</h3><p>{{age}}</p></div></body>
</html>
<script>document.addEventListener("DOMContentLoaded",function () {let opt = { el: "#app", data: { name: "等待修改...", age: 20 } };let vm = new Vue(opt);setTimeout(() => {opt.data.name = "jing";}, 2000);},false);class Vue {constructor(opt) {this.opt = opt;this.observer(opt.data);let root = document.querySelector(opt.el);this.compile(root);}observer(data) {Object.keys(data).forEach((key) => {let obv = new Dep();data["_" + key] = data[key];Object.defineProperty(data, key, {get() {Dep.target && obv.addSubNode(Dep.target);return data["_" + key];},set(newVal) {obv.update(newVal);data["_" + key] = newVal;},});});}compile(node) {[].forEach.call(node.childNodes, (child) => {if (!child.firstElementChild && /\{\{(.*)\}\}/.test(child.innerHTML)) {let key = RegExp.$1.trim();child.innerHTML = child.innerHTML.replace(new RegExp("\\{\\{\\s*" + key + "\\s*\\}\\}", "gm"),this.opt.data[key]);Dep.target = child;this.opt.data[key];Dep.target = null;} else if (child.firstElementChild) this.compile(child);});}}class Dep {constructor() {this.subNode = [];}addSubNode(node) {this.subNode.push(node);}update(newVal) {this.subNode.forEach((node) => {node.innerHTML = newVal;});}}
</script>
简化版2
function update(){console.log('数据变化~~~ mock update view')
}
let obj = [1,2,3]
// 变异方法 push shift unshfit reverse sort splice pop
// Object.defineProperty
let oldProto = Array.prototype;
let proto = Object.create(oldProto); // 克隆了一分
['push','shift'].forEach(item=>{proto[item] = function(){update();oldProto[item].apply(this,arguments);}
})
function observer(value){ // proxy reflectif(Array.isArray(value)){// AOPreturn value.__proto__ = proto;// 重写 这个数组里的push shift unshfit reverse sort splice pop}if(typeof value !== 'object'){return value;}for(let key in value){defineReactive(value,key,value[key]);}
}
function defineReactive(obj,key,value){observer(value); // 如果是对象 继续增加getter和setterObject.defineProperty(obj,key,{get(){return value;},set(newValue){if(newValue !== value){observer(newValue);value = newValue;update();}}})
}
observer(obj);
// AOP
// obj.name = {n:200}; // 数据变了 需要更新视图 深度监控
// obj.name.n = 100;
obj.push(123);
obj.push(456);
console.log(obj);
实现instanceOf
思路:
- 步骤1:先取得当前类的原型,当前实例对象的原型链
- 步骤2:一直循环(执行原型链的查找机制)
- 取得当前实例对象原型链的原型链(
proto = proto.__proto__
,沿着原型链一直向上查找) - 如果 当前实例的原型链
__proto__
上找到了当前类的原型prototype
,则返回true
- 如果 一直找到
Object.prototype.__proto__ == null
,Object
的基类(null
)上面都没找到,则返回false
- 取得当前实例对象原型链的原型链(
// 实例.__ptoto__ === 类.prototype
function _instanceof(example, classFunc) {// 由于instance要检测的是某对象,需要有一个前置判断条件//基本数据类型直接返回falseif(typeof example !== 'object' || example === null) return false;let proto = Object.getPrototypeOf(example);while(true) {if(proto == null) return false;// 在当前实例对象的原型链上,找到了当前类if(proto == classFunc.prototype) return true;// 沿着原型链__ptoto__一层一层向上查proto = Object.getPrototypeof(proto); // 等于proto.__ptoto__}
}console.log('test', _instanceof(null, Array)) // false
console.log('test', _instanceof([], Array)) // true
console.log('test', _instanceof('', Array)) // false
console.log('test', _instanceof({}, Object)) // true
实现bind方法
bind
的实现对比其他两个函数略微地复杂了一点,涉及到参数合并(类似函数柯里化),因为bind
需要返回一个函数,需要判断一些边界问题,以下是bind
的实现
bind
返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过new
的方式,我们先来说直接调用的方式- 对于直接调用来说,这里选择了
apply
的方式实现,但是对于参数需要注意以下情况:因为bind
可以实现类似这样的代码f.bind(obj, 1)(2)
,所以我们需要将两边的参数拼接起来 - 最后来说通过
new
的方式,对于new
的情况来说,不会被任何方式改变this
,所以对于这种情况我们需要忽略传入的this
简洁版本
- 对于普通函数,绑定
this
指向 - 对于构造函数,要保证原函数的原型对象上的属性不能丢失
Function.prototype.myBind = function(context = window, ...args) {// this表示调用bind的函数let self = this;//返回了一个函数,...innerArgs为实际调用时传入的参数let fBound = function(...innerArgs) { //this instanceof fBound为true表示构造函数的情况。如new func.bind(obj)// 当作为构造函数时,this 指向实例,此时 this instanceof fBound 结果为 true,可以让实例获得来自绑定函数的值// 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 contextreturn self.apply(this instanceof fBound ? this : context, args.concat(innerArgs));}// 如果绑定的是构造函数,那么需要继承构造函数原型属性和方法:保证原函数的原型对象上的属性不丢失// 实现继承的方式: 使用Object.createfBound.prototype = Object.create(this.prototype);return fBound;
}
// 测试用例function Person(name, age) {console.log('Person name:', name);console.log('Person age:', age);console.log('Person this:', this); // 构造函数this指向实例对象
}// 构造函数原型的方法
Person.prototype.say = function() {console.log('person say');
}// 普通函数
function normalFun(name, age) {console.log('普通函数 name:', name); console.log('普通函数 age:', age); console.log('普通函数 this:', this); // 普通函数this指向绑定bind的第一个参数 也就是例子中的obj
}var obj = {name: 'poetries',age: 18
}// 先测试作为构造函数调用
var bindFun = Person.myBind(obj, 'poetry1') // undefined
var a = new bindFun(10) // Person name: poetry1、Person age: 10、Person this: fBound {}
a.say() // person say// 再测试作为普通函数调用
var bindNormalFun = normalFun.myBind(obj, 'poetry2') // undefined
bindNormalFun(12) // 普通函数name: poetry2 普通函数 age: 12 普通函数 this: {name: 'poetries', age: 18}
注意:
bind
之后不能再次修改this
的指向,bind
多次后执行,函数this
还是指向第一次bind
的对象
数组中的数据根据key去重
给定一个任意数组,实现一个通用函数,让数组中的数据根据 key 排重:
const dedup = (data, getKey = () => {} ) => {// todo
}
let data = [{ id: 1, v: 1 },{ id: 2, v: 2 },{ id: 1, v: 1 },
];// 以 id 作为排重 key,执行函数得到结果
// data = [
// { id: 1, v: 1 },
// { id: 2, v: 2 },
// ];
实现
const dedup = (data, getKey = () => { }) => {const dateMap = data.reduce((pre, cur) => {const key = getKey(cur)if (!pre[key]) {pre[key] = cur}return pre}, {})return Object.values(dateMap)
}
使用
let data = [{ id: 1, v: 1 },{ id: 2, v: 2 },{ id: 1, v: 1 },
];
console.log(dedup(data, (item) => item.id))// 以 id 作为排重 key,执行函数得到结果
// data = [
// { id: 1, v: 1 },
// { id: 2, v: 2 },
// ];
实现find方法
find
接收一个方法作为参数,方法内部返回一个条件find
会遍历所有的元素,执行你给定的带有条件返回值的函数- 符合该条件的元素会作为
find
方法的返回值 - 如果遍历结束还没有符合该条件的元素,则返回
undefined
var users = [{id: 1, name: '张三'},{id: 2, name: '张三'},{id: 3, name: '张三'},{id: 4, name: '张三'}
]Array.prototype.myFind = 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 this[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
希望大家可以熟练掌握。学有余力的同学