共计 27612 个字符,预计需要花费 70 分钟才能阅读完成。
实现一个 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.length
return {
// 自定义 next 办法
next: function() {
// 如果索引还没有超出汇合长度,done 为 false
var done = idx >= len
// 如果 done 为 false,则能够持续取值
var value = !done ? list[idx++] : undefined
// 将以后值与遍历是否结束(done)返回
return {
done: done,
value: value
}
}
}
}
var iterator = iteratorGenerator(['1 号选手', '2 号选手', '3 号选手'])
iterator.next()
iterator.next()
iterator.next()
此处为了记录每次遍历的地位,咱们实现了一个闭包,借助自在变量来做咱们的迭代过程中的“游标”。
运行一下咱们自定义的迭代器,后果合乎预期:
实现单例模式
外围要点: 用闭包和
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 currentState
let 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 + 1
case 'MINUS':
return state - 1
default:
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 = ()=>currentState
const 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.dispatch
const 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 bound
return 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,则间接 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 能笼罩大多数的利用场景,没错,谈到深拷贝,我第一个想到的也是它。然而实际上,对于某些严格的场景来说,这个办法是有微小的坑的。问题如下:
- 无奈解决
循环援用
的问题。举个例子:
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;
}
实现 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 类的动态属性 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>
实现 ES6 的 extends
function B(name){this.name = name;};
function A(name,age){
//1. 将 A 的原型指向 B
Object.setPrototypeOf(A,B);
//2. 用 A 的实例作为 this 调用 B, 失去继承 B 之后的实例,这一步相当于调用 super
Object.getPrototypeOf(A).call(this, name)
//3. 将 A 原有的属性增加到新实例上
this.age = age;
//4. 返回新实例对象
return this;
};
var a = new A('poetry',22);
console.log(a);
实现 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
进行劫持,监听用户对属性进行调用以及赋值时的具体情况,从而实现的双向绑定
实现节流函数(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 工夫内再次触发的话,就会勾销之前的计时器而从新设置。这样一来,只有最初一次操作能被触发。
- 函数节流:使得肯定工夫内只触发一次函数。原理是通过判断是否达到肯定工夫来触发函数。
实现模板字符串解析性能
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 reflect
if(Array.isArray(value)){
// AOP
return 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 和 setter
Object.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 要检测的是某对象,须要有一个前置判断条件
// 根本数据类型间接返回 false
if(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 指向 context
return self.apply(
this instanceof fBound ? this : context,
args.concat(innerArgs)
);
}
// 如果绑定的是构造函数,那么须要继承构造函数原型属性和办法:保障原函数的原型对象上的属性不失落
// 实现继承的形式: 应用 Object.create
fBound.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
心愿大家能够熟练掌握。学有余力的同学