当前位置: 首页 > wzjs >正文

石家庄网站建设公司logo设计大师

石家庄网站建设公司,logo设计大师,在线做印章的网站,太谷县建设局网站实现一个compose函数 组合多个函数,从右到左,比如:compose(f, g, h) 最终得到这个结果 (...args) > f(g(h(...args))). 题目描述:实现一个 compose 函数 // 用法如下: function fn1(x) {return x 1; } function fn2(x) {return x 2; } …

实现一个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等特殊对象的克隆
  • 会抛弃对象的constructor,所有的构造函数会指向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能覆盖大多数的应用场景,没错,谈到深拷贝,我第一个想到的也是它。但是实际上,对于某些严格的场景来说,这个方法是有巨大的坑的。问题如下:

  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) {//处理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,只能是挂载到某个对象下,要么是全局的window,要么就是自定义一个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.definePropertygetset进行劫持,监听用户对属性进行调用以及赋值时的具体情况,从而实现的双向绑定

实现节流函数(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我会分为这么几步来:

  1. 首先我会定义一个类Vue,这个类接收的是一个options,那么其中可能有需要挂载的根元素的id,也就是el属性;然后应该还有一个data属性,表示需要双向绑定的数据
  2. 其次我会定义一个Dep类,这个类产生的实例对象中会定义一个subs数组用来存放所依赖这个属性的依赖,已经添加依赖的方法addSub,删除方法removeSub,还有一个notify方法用来遍历更新它subs中的所有依赖,同时Dep类有一个静态属性target它用来表示当前的观察者,当后续进行依赖收集的时候可以将它添加到dep.subs中。
  3. 然后设计一个observe方法,这个方法接收的是传进来的data,也就是options.data,里面会遍历data中的每一个属性,并使用Object.defineProperty()来重写它的getset,那么这里面呢可以使用new Dep()实例化一个dep对象,在get的时候调用其addSub方法添加当前的观察者Dep.target完成依赖收集,并且在set的时候调用dep.notify方法来通知每一个依赖它的观察者进行更新
  4. 完成这些之后,我们还需要一个compile方法来将HTML模版和数据结合起来。在这个方法中首先传入的是一个node节点,然后遍历它的所有子级,判断是否有firstElmentChild,有的话则进行递归调用compile方法,没有firstElementChild的话且该child.innderHTML用正则匹配满足有/\{\{(.*)\}\}/项的话则表示有需要双向绑定的数据,那么就将用正则new Reg('\\{\\{\\s*' + key + '\\s*\\}\\}', 'gm')替换掉是其为msg变量。
  5. 完成变量替换的同时,还需要将Dep.target指向当前的这个child,且调用一下this.opt.data[key],也就是为了触发这个数据的get来对当前的child进行依赖收集,这样下次数据变化的时候就能通知child进行视图更新了,不过在最后要记得将Dep.target指为null哦(其实在Vue中是有一个targetStack栈用来存放target的指向的)
  6. 那么最后我们只需要监听documentDOMContentLoaded然后在回调函数中实例化这个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__ == nullObject的基类(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)

大家会发现,整个调用过程中,没有出现具体的发布者和订阅者(比如上面的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希望大家可以熟练掌握。学有余力的同学


文章转载自:

http://xp4RFqMs.mdxwz.cn
http://qdrCwgNT.mdxwz.cn
http://nz7JfqJR.mdxwz.cn
http://Tiz8ev8R.mdxwz.cn
http://1q3PFfz9.mdxwz.cn
http://Bf0bmp82.mdxwz.cn
http://M5ahMCnx.mdxwz.cn
http://dLu6LQAL.mdxwz.cn
http://j2oBfiw8.mdxwz.cn
http://VNaUm326.mdxwz.cn
http://Wrf6dS95.mdxwz.cn
http://7pDkdJUq.mdxwz.cn
http://C2ZSYHQD.mdxwz.cn
http://DBO9KZVM.mdxwz.cn
http://baGqz2Zz.mdxwz.cn
http://VbOrWVdm.mdxwz.cn
http://UHYYnq0K.mdxwz.cn
http://7qZtedbG.mdxwz.cn
http://H1nGFcmo.mdxwz.cn
http://ODmeGJA0.mdxwz.cn
http://uqDLu1we.mdxwz.cn
http://58Y3OKw7.mdxwz.cn
http://0UMEQXZX.mdxwz.cn
http://7zNjzThk.mdxwz.cn
http://oujCSQWh.mdxwz.cn
http://UPzKQgih.mdxwz.cn
http://o0sPAZ2r.mdxwz.cn
http://IDlKJSUH.mdxwz.cn
http://DWb3srQ5.mdxwz.cn
http://usCA1wGW.mdxwz.cn
http://www.dtcms.com/wzjs/637877.html

相关文章:

  • 济南集团网站建设方案山东省住房和城乡建设厅电话号码
  • 在企业网站建设的解决方案中淘宝网站建设类目
  • 网站红色北京比较好的it公司
  • 中国公路建设协会网站室内装修设计师资格证怎么考
  • 网站开发哪种语言网页设计在线培训网站有哪些
  • nginx 做网站百度问一问免费咨询
  • 流量点击推广平台网站推广seo代理
  • 学校网站建设需要多少钱鄞州区卖场设计网站建设
  • 滨州论坛网站建设398做网站彩铃
  • wordpress网站前台密码建设企业网站官网u盾
  • 深圳企业网站建设公司页面模板嵌入文章内
  • 我的网站是面向全国的选哪个公司的服务器比较好wordpress顶部菜单设置
  • 网站建设外贸经典重庆论坛畅谈重庆
  • 广州网站建设技术托管信息流广告图片
  • 网站建设方为客户提供使用说明书网站服务器的安全性首先是实现用户账号的权限设置
  • 企业网站手机端太简洁网络营销是什么含义
  • 网站正建设中展示型网站开发
  • 济南网站APP2018企业网站转化率
  • 网站建设公司市场开发方案程序员培训机构排名前十
  • 做seo网站诊断书怎么做合肥的网站建设剂屏
  • 建设网站查询余额做网站在哪个地方买空间
  • 网站编辑合适内向的人做吗做网站电话销售
  • vs网站开发建表怎么肩啊大作业网站建设方案
  • 绍兴网站建设做网站公司-深圳信科
  • 做水果网站特点分析网站动态链接做Seo怎么办
  • 长沙网站运营外包公司电商创业项目有哪些
  • 网站开发全程实例课本代码wordpress取消邮件
  • 做众筹的网站wordpress 禁用编辑器
  • 用织梦做网站需不需授权同城推广平台
  • 网站加背景音乐中国高铁哪家公司建设