发布订阅模式(事件总线)
描述:实现一个发布订阅模式,拥有 on, emit, once, off 方法
class EventEmitter { constructor() { // 包含所有监听器函数的容器对象 // 内部结构: {msg1: [listener1, listener2], msg2: [listener3]} this.cache = {}; } // 实现订阅 on(name, callback) { if(this.cache[name]) { this.cache[name].push(callback); } else { this.cache[name] = [callback]; } } // 删除订阅 off(name, callback) { if(this.cache[name]) { this.cache[name] = this.cache[name].filter(item => item !== callback); } if(this.cache[name].length === 0) delete this.cache[name]; } // 只执行一次订阅事件 once(name, callback) { callback(); this.off(name, callback); } // 触发事件 emit(name, ...data) { if(this.cache[name]) { // 创建副本,如果回调函数内继续注册相同事件,会造成死循环 let tasks = this.cache[name].slice(); for(let fn of tasks) { fn(...data); } } }}复制代码
复制代码
原型修改、重写
function Person(name) { this.name = name}// 修改原型Person.prototype.getName = function() {}var p = new Person('hello')console.log(p.__proto__ === Person.prototype) // trueconsole.log(p.__proto__ === p.constructor.prototype) // true// 重写原型Person.prototype = { getName: function() {}}var p = new Person('hello')console.log(p.__proto__ === Person.prototype) // trueconsole.log(p.__proto__ === p.constructor.prototype) // false复制代码
复制代码
可以看到修改原型的时候 p 的构造函数不是指向 Person 了,因为直接给 Person 的原型对象直接用对象赋值时,它的构造函数指向的了根构造函数 Object,所以这时候p.constructor === Object ,而不是p.constructor === Person。要想成立,就要用 constructor 指回来:
Person.prototype = { getName: function() {}}var p = new Person('hello')p.constructor = Personconsole.log(p.__proto__ === Person.prototype) // trueconsole.log(p.__proto__ === p.constructor.prototype) // true
复制代码
复制代码
为什么 0.1 + 0.2 != 0.3,请详述理由
因为 JS 采用 IEEE 754 双精度版本(64 位),并且只要采用 IEEE 754 的语言都有该问题。
我们都知道计算机表示十进制是采用二进制表示的,所以 0.1 在二进制表示为
// (0011) 表示循环0.1 = 2^-4 * 1.10011(0011)复制代码
复制代码
那么如何得到这个二进制的呢,我们可以来演算下
小数算二进制和整数不同。乘法计算时,只计算小数位,整数位用作每一位的二进制,并且得到的第一位为最高位。所以我们得出 0.1 = 2^-4 * 1.10011(0011),那么 0.2 的演算也基本如上所示,只需要去掉第一步乘法,所以得出 0.2 = 2^-3 * 1.10011(0011)。
回来继续说 IEEE 754 双精度。六十四位中符号位占一位,整数位占十一位,其余五十二位都为小数位。因为 0.1 和 0.2 都是无限循环的二进制了,所以在小数位末尾处需要判断是否进位(就和十进制的四舍五入一样)。
所以 2^-4 * 1.10011...001 进位后就变成了 2^-4 * 1.10011(0011 * 12次)010 。那么把这两个二进制加起来会得出 2^-2 * 1.0011(0011 * 11次)0100 , 这个值算成十进制就是 0.30000000000000004
下面说一下原生解决办法,如下代码所示
parseFloat((0.1 + 0.2).toFixed(10))复制代码
复制代码
事件流
事件流是网页元素接收事件的顺序,"DOM2 级事件"规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。首先发生的事件捕获,为截获事件提供机会。然后是实际的目标接受事件。最后一个阶段是时间冒泡阶段,可以在这个阶段对事件做出响应。虽然捕获阶段在规范中规定不允许响应事件,但是实际上还是会执行,所以有两次机会获取到目标对象。
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>事件冒泡</title></head><body> <div> <p id="parEle">我是父元素 <span id="sonEle">我是子元素</span></p> </div></body></html><script type="text/javascript">var sonEle = document.getElementById('sonEle');var parEle = document.getElementById('parEle');parEle.addEventListener('click', function () { alert('父级 冒泡');}, false);parEle.addEventListener('click', function () { alert('父级 捕获');}, true);sonEle.addEventListener('click', function () { alert('子级冒泡');}, false);sonEle.addEventListener('click', function () { alert('子级捕获');}, true);
</script>复制代码
复制代码
当容器元素及嵌套元素,即在捕获阶段又在冒泡阶段调用事件处理程序时:事件按 DOM 事件流的顺序执行事件处理程序:
且当事件处于目标阶段时,事件调用顺序决定于绑定事件的书写顺序,按上面的例子为,先调用冒泡阶段的事件处理程序,再调用捕获阶段的事件处理程序。依次 alert 出“子集冒泡”,“子集捕获”。
事件是如何实现的?
基于发布订阅模式,就是在浏览器加载的时候会读取事件相关的代码,但是只有实际等到具体的事件触发的时候才会执行。
比如点击按钮,这是个事件(Event),而负责处理事件的代码段通常被称为事件处理程序(Event Handler),也就是「启动对话框的显示」这个动作。
在 Web 端,我们常见的就是 DOM 事件:
DOM0 级事件,直接在 html 元素上绑定 on-event,比如 onclick,取消的话,dom.onclick = null,同一个事件只能有一个处理程序,后面的会覆盖前面的。
DOM2 级事件,通过 addEventListener 注册事件,通过 removeEventListener 来删除事件,一个事件可以有多个事件处理程序,按顺序执行,捕获事件和冒泡事件
DOM3 级事件,增加了事件类型,比如 UI 事件,焦点事件,鼠标事件
JS 隐式转换,显示转换
一般非基础类型进行转换时会先调用 valueOf,如果 valueOf 无法返回基本类型值,就会调用 toString
字符串和数字
布尔值到数字
1 + true = 2
1 + false = 1
转换为布尔值
for 中第二个
while
if
三元表达式
|| (逻辑或) && (逻辑与)左边的操作数
符号
不能被转换为数字
能被转换为布尔值(都是 true)
可以被转换成字符串 "Symbol(cool)"
宽松相等和严格相等
宽松相等允许进行强制类型转换,而严格相等不允许
字符串与数字
转换为数字然后比较
其他类型与布尔类型
对象与非对象
假值列表
undefined
null
false
+0, -0, NaN
""
IE 兼容
attchEvent('on' + type, handler)
detachEvent('on' + type, handler)
代码输出结果
function SuperType(){ this.property = true;}
SuperType.prototype.getSuperValue = function(){ return this.property;};
function SubType(){ this.subproperty = false;}
SubType.prototype = new SuperType();SubType.prototype.getSubValue = function (){ return this.subproperty;};
var instance = new SubType();console.log(instance.getSuperValue());复制代码
复制代码
输出结果:true
实际上,这段代码就是在实现原型链继承,SubType 继承了 SuperType,本质是重写了 SubType 的原型对象,代之以一个新类型的实例。SubType 的原型被重写了,所以 instance.constructor 指向的是 SuperType。具体如下:
基于 Localstorage 设计一个 1M 的缓存系统,需要实现缓存淘汰机制
设计思路如下:
存储的每个对象需要添加两个属性:分别是过期时间和存储时间。
利用一个属性保存系统中目前所占空间大小,每次存储都增加该属性。当该属性值大于 1M 时,需要按照时间排序系统中的数据,删除一定量的数据保证能够存储下目前需要存储的数据。
每次取数据时,需要判断该缓存数据是否过期,如果过期就删除。
以下是代码实现,实现了思路,但是可能会存在 Bug,但是这种设计题一般是给出设计思路和部分代码,不会需要写出一个无问题的代码
class Store { constructor() { let store = localStorage.getItem('cache') if (!store) { store = { maxSize: 1024 * 1024, size: 0 } this.store = store } else { this.store = JSON.parse(store) } } set(key, value, expire) { this.store[key] = { date: Date.now(), expire, value } let size = this.sizeOf(JSON.stringify(this.store[key])) if (this.store.maxSize < size + this.store.size) { console.log('超了-----------'); var keys = Object.keys(this.store); // 时间排序 keys = keys.sort((a, b) => { let item1 = this.store[a], item2 = this.store[b]; return item2.date - item1.date; }); while (size + this.store.size > this.store.maxSize) { let index = keys[keys.length - 1] this.store.size -= this.sizeOf(JSON.stringify(this.store[index])) delete this.store[index] } } this.store.size += size
localStorage.setItem('cache', JSON.stringify(this.store)) } get(key) { let d = this.store[key] if (!d) { console.log('找不到该属性'); return } if (d.expire > Date.now) { console.log('过期删除'); delete this.store[key] localStorage.setItem('cache', JSON.stringify(this.store)) } else { return d.value } } sizeOf(str, charset) { var total = 0, charCode, i, len; charset = charset ? charset.toLowerCase() : ''; if (charset === 'utf-16' || charset === 'utf16') { for (i = 0, len = str.length; i < len; i++) { charCode = str.charCodeAt(i); if (charCode <= 0xffff) { total += 2; } else { total += 4; } } } else { for (i = 0, len = str.length; i < len; i++) { charCode = str.charCodeAt(i); if (charCode <= 0x007f) { total += 1; } else if (charCode <= 0x07ff) { total += 2; } else if (charCode <= 0xffff) { total += 3; } else { total += 4; } } } return total; }}复制代码
复制代码
10 个 Ajax 同时发起请求,全部返回展示结果,并且至多允许三次失败,说出设计思路
这个问题相信很多人会第一时间想到 Promise.all ,但是这个函数有一个局限在于如果失败一次就返回了,直接这样实现会有点问题,需要变通下。以下是两种实现思路
// 以下是不完整代码,着重于思路 非 Promise 写法let successCount = 0let errorCount = 0let datas = []ajax(url, (res) => { if (success) { success++ if (success + errorCount === 10) { console.log(datas) } else { datas.push(res.data) } } else { errorCount++ if (errorCount > 3) { // 失败次数大于3次就应该报错了 throw Error('失败三次') } }})// Promise 写法let errorCount = 0let p = new Promise((resolve, reject) => { if (success) { resolve(res.data) } else { errorCount++ if (errorCount > 3) { // 失败次数大于3次就应该报错了 reject(error) } else { resolve(error) } }})Promise.all([p]).then(v => { console.log(v);});复制代码
复制代码
说一下原型链和原型链的继承吧
function Person(name) { this.name = name;}
Person.prototype.constructor = Person复制代码
复制代码
在发生 new 构造函数调用时,会将创建的新对象的 [[Prototype]] 链接到 Person.prototype 指向的对象,这个机制就被称为原型链继承
方法定义在原型上,属性定义在构造函数上
首先要说一下 JS 原型和实例的关系:每个构造函数 (constructor)都有一个原型对象(prototype),这个原型对象包含一个指向此构造函数的指针属性,通过 new 进行构造函数调用生成的实例,此实例包含一个指向原型对象的指针,也就是通过 [[Prototype]] 链接到了这个原型对象
然后说一下 JS 中属性的查找:当我们试图引用实例对象的某个属性时,是按照这样的方式去查找的,首先查找实例对象上是否有这个属性,如果没有找到,就去构造这个实例对象的构造函数的 prototype 所指向的对象上去查找,如果还找不到,就从这个 prototype 对象所指向的构造函数的 prototype 原型对象上去查找
什么是原型链:这样逐级查找形似一个链条,且通过 [[Prototype]] 属性链接,所以被称为原型链
什么是原型链继承,类比类的继承:当有两个构造函数 A 和 B,将一个构造函数 A 的原型对象的,通过其 [[Prototype]] 属性链接到另外一个 B 构造函数的原型对象时,这个过程被称之为原型继承。
** 标准答案更正确的解释**
什么是原型链?
当对象查找一个属性的时候,如果没有在自身找到,那么就会查找自身的原型,如果原型还没有找到,那么会继续查找原型的原型,直到找到 Object.prototype 的原型时,此时原型为 null,查找停止。这种通过 通过原型链接的逐级向上的查找链被称为原型链
什么是原型继承?
一个对象可以使用另外一个对象的属性或者方法,就称之为继承。具体是通过将这个对象的原型设置为另外一个对象,这样根据原型链的规则,如果查找一个对象属性且在自身不存在时,就会查找另外一个对象,相当于一个对象可以使用另外一个对象的属性和方法了。
如果 new 一个箭头函数的会怎么样
箭头函数是 ES6 中的提出来的,它没有 prototype,也没有自己的 this 指向,更不可以使用 arguments 参数,所以不能 New 一个箭头函数。
new 操作符的实现步骤如下:
创建一个对象
将构造函数的作用域赋给新对象(也就是将对象的__proto__属性指向构造函数的 prototype 属性)
指向构造函数中的代码,构造函数中的 this 指向该对象(也就是为这个对象添加属性和方法)
返回新的对象
所以,上面的第二、三步,箭头函数都是没有办法执行的。
实现数组原型方法
forEach
语法:arr.forEach(callback(currentValue [, index [, array]])[, thisArg])
参数:
callback:为数组中每个元素执行的函数,该函数接受 1-3 个参数currentValue: 数组中正在处理的当前元素index(可选): 数组中正在处理的当前元素的索引array(可选): forEach() 方法正在操作的数组 thisArg(可选): 当执行回调函数 callback 时,用作 this 的值。
返回值:undefined
Array.prototype.forEach1 = function(callback, thisArg) { if(this == null) { throw new TypeError('this is null or not defined'); } if(typeof callback !== "function") { throw new TypeError(callback + 'is not a function'); } // 创建一个新的 Object 对象。该对象将会包裹(wrapper)传入的参数 this(当前数组)。 const O = Object(this); // O.length >>> 0 无符号右移 0 位 // 意义:为了保证转换后的值为正整数。 // 其实底层做了 2 层转换,第一是非 number 转成 number 类型,第二是将 number 转成 Uint32 类型 const len = O.length >>> 0; let k = 0; while(k < len) { if(k in O) { callback.call(thisArg, O[k], k, O); } k++; }}复制代码
复制代码
map
语法: arr.map(callback(currentValue [, index [, array]])[, thisArg])
参数:与 forEach() 方法一样
返回值:一个由原数组每个元素执行回调函数的结果组成的新数组。
Array.prototype.map1 = function(callback, thisArg) { if(this == null) { throw new TypeError('this is null or not defined'); } if(typeof callback !== "function") { throw new TypeError(callback + 'is not a function'); } const O = Object(this); const len = O.length >>> 0; let newArr = []; // 返回的新数组 let k = 0; while(k < len) { if(k in O) { newArr[k] = callback.call(thisArg, O[k], k, O); } k++; } return newArr;}复制代码
复制代码
filter
语法:arr.filter(callback(element [, index [, array]])[, thisArg])
参数:
callback: 用来测试数组的每个元素的函数。返回 true 表示该元素通过测试,保留该元素,false 则不保留。它接受以下三个参数:element、index、array,参数的意义与 forEach 一样。
thisArg(可选): 执行 callback 时,用于 this 的值。
返回值:一个新的、由通过测试的元素组成的数组,如果没有任何数组元素通过测试,则返回空数组。
Array.prototype.filter1 = function(callback, thisArg) { if(this == null) { throw new TypeError('this is null or not defined'); } if(typeof callback !== "function") { throw new TypeError(callback + 'is not a function'); } const O = Object(this); const len = O.length >>> 0; let newArr = []; // 返回的新数组 let k = 0; while(k < len) { if(k in O) { if(callback.call(thisArg, O[k], k, O)) { newArr.push(O[k]); } } k++; } return newArr;}复制代码
复制代码
some
语法:arr.some(callback(element [, index [, array]])[, thisArg])
参数:
callback: 用来测试数组的每个元素的函数。接受以下三个参数:element、index、array,参数的意义与 forEach 一样。
thisArg(可选): 执行 callback 时,用于 this 的值。返回值:数组中有至少一个元素通过回调函数的测试就会返回 true;所有元素都没有通过回调函数的测试返回值才会为 false。
Array.prototype.some1 = function(callback, thisArg) { if(this == null) { throw new TypeError('this is null or not defined'); } if(typeof callback !== "function") { throw new TypeError(callback + 'is not a function'); } const O = Object(this); const len = O.length >>> 0; let k = 0; while(k < len) { if(k in O) { if(callback.call(thisArg, O[k], k, O)) { return true } } k++; } return false;}复制代码
复制代码
reduce
语法:arr.reduce(callback(preVal, curVal[, curIndex [, array]])[, initialValue])
参数:
callback: 一个 “reducer” 函数,包含四个参数:
preVal:上一次调用 callback 时的返回值。在第一次调用时,若指定了初始值 initialValue,其值则为 initialValue,否则为数组索引为 0 的元素 array[0]。
curVal:数组中正在处理的元素。在第一次调用时,若指定了初始值 initialValue,其值则为数组索引为 0 的元素 array[0],否则为 array[1]。
curIndex(可选):数组中正在处理的元素的索引。若指定了初始值 initialValue,则起始索引号为 0,否则从索引 1 起始。
array(可选):用于遍历的数组。initialValue(可选): 作为第一次调用 callback 函数时参数 preVal 的值。若指定了初始值 initialValue,则 curVal 则将使用数组第一个元素;否则 preVal 将使用数组第一个元素,而 curVal 将使用数组第二个元素。返回值:使用 “reducer” 回调函数遍历整个数组后的结果。
Array.prototype.reduce1 = function(callback, initialValue) { if(this == null) { throw new TypeError('this is null or not defined'); } if(typeof callback !== "function") { throw new TypeError(callback + 'is not a function'); } const O = Object(this); const len = O.length >>> 0; let k = 0; let accumulator = initialValue; // 如果第二个参数为undefined的情况下,则数组的第一个有效值(非empty)作为累加器的初始值 if(accumulator === undefined) { while(k < len && !(k in O)) { k++; } // 如果超出数组界限还没有找到累加器的初始值,则TypeError if(k >= len) { throw new TypeError('Reduce of empty array with no initial value'); } accumulator = O[k++]; } while(k < len) { if(k in O) { accumulator = callback(accumulator, O[k], k, O); } k++; } return accumulator;}复制代码
复制代码
类数组转化为数组的方法
题目描述:类数组拥有 length 属性 可以使用下标来访问元素 但是不能使用数组的方法 如何把类数组转化为数组?
实现代码如下:
const arrayLike=document.querySelectorAll('div')
// 1.扩展运算符[...arrayLike]// 2.Array.fromArray.from(arrayLike)// 3.Array.prototype.sliceArray.prototype.slice.call(arrayLike)// 4.Array.applyArray.apply(null, arrayLike)// 5.Array.prototype.concatArray.prototype.concat.apply([], arrayLike)
复制代码
复制代码
new 操作符
题目描述:手写 new 操作符实现
实现代码如下:
function myNew(fn, ...args) { let obj = Object.create(fn.prototype); let res = fn.call(obj, ...args); if (res && (typeof res === "object" || typeof res === "function")) { return res; } return obj;}用法如下:// // function Person(name, age) {// // this.name = name;// // this.age = age;// // }// // Person.prototype.say = function() {// // console.log(this.age);// // };// // let p1 = myNew(Person, "lihua", 18);// // console.log(p1.name);// // console.log(p1);// // p1.say();复制代码
复制代码
写代码:实现函数能够深度克隆基本类型
浅克隆:
function shallowClone(obj) { let cloneObj = {};
for (let i in obj) { cloneObj[i] = obj[i]; }
return cloneObj;}复制代码
复制代码
深克隆:
function deepCopy(obj) { if (typeof obj === 'object') { var result = obj.constructor === Array ? [] : {};
for (var i in obj) { result[i] = typeof obj[i] === 'object' ? deepCopy(obj[i]) : obj[i]; } } else { var result = obj; }
return result;}复制代码
复制代码
ES6 中模板语法与字符串处理
ES6 提出了“模板语法”的概念。在 ES6 以前,拼接字符串是很麻烦的事情:
var name = 'css' var career = 'coder' var hobby = ['coding', 'writing']var finalString = 'my name is ' + name + ', I work as a ' + career + ', I love ' + hobby[0] + ' and ' + hobby[1]复制代码
复制代码
仅仅几个变量,写了这么多加号,还要时刻小心里面的空格和标点符号有没有跟错地方。但是有了模板字符串,拼接难度直线下降:
var name = 'css' var career = 'coder' var hobby = ['coding', 'writing']var finalString = `my name is ${name}, I work as a ${career} I love ${hobby[0]} and ${hobby[1]}`复制代码
复制代码
字符串不仅更容易拼了,也更易读了,代码整体的质量都变高了。这就是模板字符串的第一个优势——允许用 ${}的方式嵌入变量。但这还不是问题的关键,模板字符串的关键优势有两个:
基于第一点,可以在模板字符串里无障碍地直接写 html 代码:
let list = ` <ul> <li>列表项1</li> <li>列表项2</li> </ul>`;console.log(message); // 正确输出,不存在报错复制代码
复制代码
基于第二点,可以把一些简单的计算和调用丢进 ${} 来做:
function add(a, b) { const finalString = `${a} + ${b} = ${a+b}` console.log(finalString)}add(1, 2) // 输出 '1 + 2 = 3'复制代码
复制代码
除了模板语法外, ES6 中还新增了一系列的字符串方法用于提升开发效率:
(1)存在性判定:在过去,当判断一个字符/字符串是否在某字符串中时,只能用 indexOf > -1 来做。现在 ES6 提供了三个方法:includes、startsWith、endsWith,它们都会返回一个布尔值来告诉你是否存在。
const son = 'haha' const father = 'xixi haha hehe'father.includes(son) // true复制代码
复制代码
const father = 'xixi haha hehe'father.startsWith('haha') // falsefather.startsWith('xixi') // true复制代码
复制代码
const father = 'xixi haha hehe' father.endsWith('hehe') // true复制代码
复制代码
(2)自动重复:可以使用 repeat 方法来使同一个字符串输出多次(被连续复制多次):
const sourceCode = 'repeat for 3 times;'const repeated = sourceCode.repeat(3) console.log(repeated) // repeat for 3 times;repeat for 3 times;repeat for 3 times;复制代码
复制代码
iframe 有那些优点和缺点?
iframe 元素会创建包含另外一个文档的内联框架(即行内框架)。
优点:
用来加载速度较慢的内容(如广告)
可以使脚本可以并行下载
可以实现跨子域通信
缺点:
iframe 会阻塞主页面的 onload 事件
无法被一些搜索引擎索识别
会产生很多页面,不容易管理
const 对象的属性可以修改吗
const 保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。对于基本类型的数据(数值、字符串、布尔值),其值就保存在变量指向的那个内存地址,因此等同于常量。
但对于引用类型的数据(主要是对象和数组)来说,变量指向数据的内存地址,保存的只是一个指针,const 只能保证这个指针是固定不变的,至于它指向的数据结构是不是可变的,就完全不能控制了。
评论