用正则写一个根据 name 获取 cookie 中的值的方法
function getCookie(name) { var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]*)')); if (match) return unescape(match[2]);}
复制代码
获取页面上的cookie可以使用 document.cookie
这里获取到的是类似于这样的字符串:
'username=poetry; user-id=12345; user-roles=home, me, setting'
复制代码
可以看到这么几个信息:
每一个 cookie 都是由 name=value 这样的形式存储的
每一项的开头可能是一个空串''(比如username的开头其实就是), 也可能是一个空字符串' '(比如user-id的开头就是)
每一项用";"来区分
如果某项中有多个值的时候,是用","来连接的(比如user-roles的值)
每一项的结尾可能是有";"的(比如username的结尾),也可能是没有的(比如user-roles的结尾)
所以我们将这里的正则拆分一下:
'(^| )'表示的就是获取每一项的开头,因为我们知道如果^不是放在[]里的话就是表示开头匹配。所以这里(^| )的意思其实就被拆分为(^)表示的匹配username这种情况,它前面什么都没有是一个空串(你可以把(^)理解为^它后面还有一个隐藏的'');而|表示的就是或者是一个" "(为了匹配user-id开头的这种情况)
+name+这没什么好说的
=([^;]*)这里匹配的就是=后面的值了,比如poetry;刚刚说了^要是放在[]里的话就表示"除了^后面的内容都能匹配",也就是非的意思。所以这里([^;]*)表示的是除了";"这个字符串别的都匹配(*应该都知道什么意思吧,匹配 0 次或多次)
有的大佬等号后面是这样写的'=([^;]*)(;|$)',而最后为什么可以把'(;|$)'给省略呢?因为其实最后一个cookie项是没有';'的,所以它可以合并到=([^;]*)这一步。
最后获取到的match其实是一个长度为 4 的数组。比如:
[ "username=poetry;", "", "poetry", ";"]
复制代码
第 0 项:全量
第 1 项:开头
第 2 项:中间的值
第 3 项:结尾
所以我们是要拿第 2 项match[2]的值。
为了防止获取到的值是%xxx这样的字符序列,需要用unescape()方法解码。
前端手写面试题详细解答
滚动加载
原理就是监听页面滚动事件,分析 clientHeight、scrollTop、scrollHeight 三者的属性关系。
window.addEventListener('scroll', function() { const clientHeight = document.documentElement.clientHeight; const scrollTop = document.documentElement.scrollTop; const scrollHeight = document.documentElement.scrollHeight; if (clientHeight + scrollTop >= scrollHeight) { // 检测到滚动至页面底部,进行后续操作 // ... }}, false);
复制代码
实现迭代器生成函数
我们说迭代器对象全凭迭代器生成函数帮我们生成。在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()
复制代码
此处为了记录每次遍历的位置,我们实现了一个闭包,借助自由变量来做我们的迭代过程中的“游标”。
运行一下我们自定义的迭代器,结果符合预期:
实现 Ajax
步骤
了解了属性和方法之后,根据 AJAX 的步骤,手写最简单的 GET 请求。
实现每隔一秒打印 1,2,3,4
// 使用闭包实现for (var i = 0; i < 5; i++) { (function(i) { setTimeout(function() { console.log(i); }, i * 1000); })(i);}// 使用 let 块级作用域for (let i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, i * 1000);}
复制代码
数组去重
const arr = [1, 1, '1', 17, true, true, false, false, 'true', 'a', {}, {}];// => [1, '1', 17, true, false, 'true', 'a', {}, {}]
复制代码
方法一:利用 Set
const res1 = Array.from(new Set(arr));
复制代码
方法二:两层 for 循环+splice
const unique1 = arr => { let len = arr.length; for (let i = 0; i < len; i++) { for (let j = i + 1; j < len; j++) { if (arr[i] === arr[j]) { arr.splice(j, 1); // 每删除一个树,j--保证j的值经过自加后不变。同时,len--,减少循环次数提升性能 len--; j--; } } } return arr;}
复制代码
方法三:利用 indexOf
const unique2 = arr => { const res = []; for (let i = 0; i < arr.length; i++) { if (res.indexOf(arr[i]) === -1) res.push(arr[i]); } return res;}
复制代码
当然也可以用 include、filter,思路大同小异。
方法四:利用 include
const unique3 = arr => { const res = []; for (let i = 0; i < arr.length; i++) { if (!res.includes(arr[i])) res.push(arr[i]); } return res;}
复制代码
方法五:利用 filter
const unique4 = arr => { return arr.filter((item, index) => { return arr.indexOf(item) === index; });}
复制代码
方法六:利用 Map
const unique5 = arr => { const map = new Map(); const res = []; for (let i = 0; i < arr.length; i++) { if (!map.has(arr[i])) { map.set(arr[i], true) res.push(arr[i]); } } return res;}
复制代码
深拷贝
递归的完整版本(考虑到了 Symbol 属性):
const cloneDeep1 = (target, hash = new WeakMap()) => { // 对于传入参数处理 if (typeof target !== 'object' || target === null) { return target; } // 哈希表中存在直接返回 if (hash.has(target)) return hash.get(target);
const cloneTarget = Array.isArray(target) ? [] : {}; hash.set(target, cloneTarget);
// 针对Symbol属性 const symKeys = Object.getOwnPropertySymbols(target); if (symKeys.length) { symKeys.forEach(symKey => { if (typeof target[symKey] === 'object' && target[symKey] !== null) { cloneTarget[symKey] = cloneDeep1(target[symKey]); } else { cloneTarget[symKey] = target[symKey]; } }) }
for (const i in target) { if (Object.prototype.hasOwnProperty.call(target, i)) { cloneTarget[i] = typeof target[i] === 'object' && target[i] !== null ? cloneDeep1(target[i], hash) : target[i]; } } return cloneTarget;}
复制代码
原型继承
这里只写寄生组合继承了,中间还有几个演变过来的继承但都有一些缺陷
function Parent() { this.name = 'parent';}function Child() { Parent.call(this); this.type = 'children';}Child.prototype = Object.create(Parent.prototype);Child.prototype.constructor = Child;
复制代码
实现类数组转化为数组
类数组转换为数组的方法有这样几种:
Array.prototype.slice.call(arrayLike);
复制代码
Array.prototype.splice.call(arrayLike, 0);
复制代码
Array.prototype.concat.apply([], arrayLike);
复制代码
打印出当前网页使用了多少种 HTML 元素
一行代码可以解决:
const fn = () => { return [...new Set([...document.querySelectorAll('*')].map(el => el.tagName))].length;}
复制代码
值得注意的是:DOM 操作返回的是类数组,需要转换为数组之后才可以调用数组的方法。
实现节流函数(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); } }}
复制代码
适用场景:
总结
Array.prototype.map()
Array.prototype.map = function(callback, thisArg) { if (this == undefined) { throw new TypeError('this is null or not defined'); } if (typeof callback !== 'function') { throw new TypeError(callback + ' is not a function'); } const res = []; // 同理 const O = Object(this); const len = O.length >>> 0; for (let i = 0; i < len; i++) { if (i in O) { // 调用回调函数并传入新数组 res[i] = callback.call(thisArg, O[i], i, this); } } return res;}
复制代码
Object.is
Object.is解决的主要是这两个问题:
+0 === -0 // trueNaN === NaN // false
复制代码
const is= (x, y) => { if (x === y) { // +0和-0应该不相等 return x !== 0 || y !== 0 || 1/x === 1/y; } else { return x !== x && y !== y; }}
复制代码
debounce(防抖)
触发高频时间后 n 秒内函数只会执行一次,如果 n 秒内高频时间再次触发,则重新计算时间。
const debounce = (fn, time) => { let timeout = null; return function() { clearTimeout(timeout) timeout = setTimeout(() => { fn.apply(this, arguments); }, time); }};
复制代码
防抖常应用于用户进行搜索输入节约请求资源,window触发resize事件时进行防抖只触发一次。
实现 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);
复制代码
查找文章中出现频率最高的单词
function findMostWord(article) { // 合法性判断 if (!article) return; // 参数处理 article = article.trim().toLowerCase(); let wordList = article.match(/[a-z]+/g), visited = [], maxNum = 0, maxWord = ""; article = " " + wordList.join(" ") + " "; // 遍历判断单词出现次数 wordList.forEach(function(item) { if (visited.indexOf(item) < 0) { // 加入 visited visited.push(item); let word = new RegExp(" " + item + " ", "g"), num = article.match(word).length; if (num > maxNum) { maxNum = num; maxWord = item; } } }); return maxWord + " " + maxNum;}
复制代码
实现深拷贝
简洁版本
简单版:
const newObj = JSON.parse(JSON.stringify(oldObj));
复制代码
局限性:
面试简版
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;}
复制代码
使用 reduce 求和
arr = [1,2,3,4,5,6,7,8,9,10],求和
let arr = [1,2,3,4,5,6,7,8,9,10]arr.reduce((prev, cur) => { return prev + cur }, 0)
复制代码
arr = [1,2,3,[[4,5],6],7,8,9],求和
let arr = [1,2,3,4,5,6,7,8,9,10]arr.flat(Infinity).reduce((prev, cur) => { return prev + cur }, 0)
复制代码
arr = [{a:1, b:3}, {a:2, b:3, c:4}, {a:3}],求和
let arr = [{a:9, b:3, c:4}, {a:1, b:3}, {a:3}]
arr.reduce((prev, cur) => { return prev + cur["a"];}, 0)
复制代码
实现一个简易的 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 :
需要注意的点:
完整代码如下:
<!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.definePropertylet 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);
复制代码
Array.prototype.filter()
Array.prototype.filter = function(callback, thisArg) { if (this == undefined) { throw new TypeError('this is null or not undefined'); } if (typeof callback !== 'function') { throw new TypeError(callback + 'is not a function'); } const res = []; // 让O成为回调函数的对象传递(强制转换对象) const O = Object(this); // >>>0 保证len为number,且为正整数 const len = O.length >>> 0; for (let i = 0; i < len; i++) { // 检查i是否在O的属性(会检查原型链) if (i in O) { // 回调函数调用传参 if (callback.call(thisArg, O[i], i, O)) { res.push(O[i]); } } } return res;}
复制代码
评论