1、求两个日期之间的有效日期:
答案:
function rangeDay(day1,day2) {
const result = []
const dayTimes = 24*60*60*1000 // ms和day换算
const startTime = day1.getTime()
const range = day2.getTime()-startTime
let total = 0
while(total <= range && range > 0){
result.push(new Date(startTime + total).toLocalDateString().replace(/\//g,'-'))
total += dayTimes
}
return result
};
rangeDay(new Date('2015-02-08'),new Date('2015-03-03'))
let rangeDay = (start,end)=>{
let oneDay = 24*60*60*1000,
dateList = '【'
start =+new Date(start)
end =+ new Date(end)
while(start < end){
let curr = new Date(start)
dateList += `${curr.getFullYear()}-${curr.getMonth()+1}-${curr.getDate()}`
start += oneDay
}
return dateList.slice(0,-1)+'】'
}
let result = rangeDay('2015-2-8','2015-3-3')
console.log(result)
2、['1', '2', '3'].map(parseInt)
what & why ?
答案:[1, NaN, NaN],先看一下map()的定义:map()的作用是返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值,定义如下:
array.map(function(currentValue,index,arr), thisValue)
其中参数function(currentValue,index,arr)是必须的,数组中每个元素都会执行这个函数,函数中又包含了三个参数,其中第一个参数是必须的,表示当前元素的值,后面两个参数可选,index表示当前元素的索引值,arr表示当前元素属于的数组对象。
map()中第二个元素是可选的,对象作为执行回调时使用,传递给函数,如果省略了 thisValue,或者传入 null、undefined,那么回调函数的 this 为全局对象。
parseInt则是用来解析字符串的,使字符串成为指定基数的整数:
parseInt(string, radix)
接收两个参数,第一个表示被处理的值(字符串),第二个表示为解析时的基数。注意,问题就出现在第二个参数上!
这样相当于parseInt每次传入了两个参数,后面的参数是parseInt中的基数(该值介于 2 ~ 36 之间。如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数,如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN)也就是map()中的index索引值(分别是0,1,2):
parseInt('1', 0)
parseInt('2', 1)
parseInt('3', 2)
其中parseInt('1',0)没有什么问题,当0作为基数,前面的字符串将会以二进制被解析,结果是1;
parseInt('2',1),基数是1(1进制),它表示的数中,最大值应该小于2,所以无法解析,结果是NaN;
parseInt('3',2)基数是2(2进制),3不是二进制数,无法解析,结果是NaN。
3、什么是防抖和节流?有什么区别?如何实现?
答案:前端会遇到一些频繁触发的例子:
inex.html文件:
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="IE=edge, chrome=1">
<title>debounce</title>
<style>
#container{
width: 100%; height: 200px; line-height: 200px; text-align: center; color: #fff; background-color: #444; font-size: 30px;
}
</style>
</head>
<body>
<div id="container"></div>
<script src="debounce.js"></script>
</body>
</html>
debounce.js文件:
var count = 1;
var container = document.getElementById('container');
function getUserAction() {
container.innerHTML = count++; // 页面上显示累加计数的数字
};
container.onmousemove = getUserAction; // 捕捉网页元素上的动作
这样在container容器中只要检测到鼠标移动,就会count++
在遇到复杂的回调函数或者ajax请求时,可能会出现卡顿现象,这个时候的解决方法就是debounce防抖或者throttle节流。
(1)防抖:防抖的原理就是你尽管触发事件,但是我一定在事件触发 n 秒后才执行。如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行。
第一版代码:
function debounce(func,wait) {
var timeout;
return function() {
clearTimeout(timeout)
timeout = setTimeout(func,wait);
}
}
要使用它:
container.onmousemove = debounch(getUserAction,1000); // 1000ms内只能触发一次,1000ms内不再触发,才会执行事件
这样调用次数大大降低了,棒棒哒,下面接着完善它:
如果我们在getUserAction函数中console.log(this),在不使用debounce函数的时候,this的值为:<div id="container"></div>
但是如果我们使用了debounce函数,this就会指向window对象!
我们需要将this指向正确的对象:
// 第二版
function debounce(func,wait) {
var timeout;
return function() {
var context = this;
clearTimeout(timeout)
timeout = setTimeout(function() {
func.apply(context) // context这个对象将代替func类里this对象
// 这样做就能让debounce里面的this指向getUserAction里的this,也就是将func中的属性传了进来
},wait);
}
}
现在this已经可以正确指向了,下面我们看下一个问题:
JavaScript 在事件处理函数中会提供事件对象 event,我们修改下 getUserAction 函数:
function getUserAvtion(e) {
console.log(e);
container.innerHTML = count++;
};
如果我们不使用 debouce 函数,这里会打印 MouseEvent 对象,但是在我们实现的 debounce 函数中,却只会打印 undefined!
我们再修改一下代码:
// 第三版
function debounce(func,wait) {
var timeout;
return function() {
var context = this;
var args = arguments;
clearTimeout(timeout)
timeout = setTimeout(function() {
func.apply(context,args)
},wait);
}
}
(2)节流:节流的原理很简单,如果你持续触发事件,每隔一段时间,只执行一次事件。
根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同:我们用 leading 代表首次是否执行,trailing 代表结束后是否再执行一次。
关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。
第一种方法:使用时间戳:当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。
// 第一版
function throttle(func,wait) {
var context,args;
var previous = 0;
return function() {
var now = +new Date();
context = this;
args = arguments;
if(now - previous > wait) { // 当前时间戳减去过去的时间戳是否大于周期
func.apply(context,args);
previous = now;
}
}
要使用它:
container.onmousemove = throttle(getUserAction, 1000);
第二种方法:使用定时器:当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。
// 第二版
function throttle(func,wait) {
var timeout;
var previous = 0;
return function() {
context = this;
args = arguments;
if(!timeout) {
timeout = setTimeout(function() {
timeout = null;
func.apply(context,args)
},wait)
}
}
}
为了让效果更加明显,我们设置 wait 的时间为 3s,这时的效果为:
当鼠标移入的时候,事件不会立刻执行,晃了 3s 后终于执行了一次,此后每 3s 执行一次,当数字显示为 3 的时候,立刻移出鼠标,相当于大约 9.2s 的时候停止触发,但是依然会在第 12s 的时候执行一次事件。
所以比较两个方法:
- 第一种事件会立刻执行,第二种事件会在 n 秒后第一次执行;
- 第一种事件停止触发后没有办法再执行事件,第二种事件停止触发后依然会再执行一次事件。
4、介绍下 Set、Map、WeakSet 和 WeakMap 的区别?
答案:
Set:是一种叫做集合的数据结构
- 成员是唯一、无序且不重复的;
- [value, value]键值与键名是一致的,或者说只有健值,没有健名,有点类似数组;
- 可以遍历,方法有add, delete,has。
weakSet
- 成员都是对象(WeakSet 只能储存对象引用,不能存放值,而 Set 对象都可以);
- 成员都是弱引用,随时可以消失(即垃圾回收机制不考虑 WeakSet 对该对象的应用),可以用来保存DOM节点,不容易造成内存泄漏,WeakSet 对象里有多少个成员元素,取决于垃圾回收机制有没有运行;
- 不能遍历,也没有办法拿到它包含的所有元素,方法有add, delete,has。
Map:是一种叫做字典的数据结构(集合与字典的区别:共同点:集合、字典可以储存不重复的值;不同点:集合是以 [value, value]的形式储存元素,字典 是以 [key, value] 的形式储存)
- 本质上是健值对的集合,类似集合;
- 可以遍历,方法很多,可以跟各种数据格式转换。
weakMap
- 直接受对象作为健名(null除外),不接受其他类型的值作为健名;
- 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的;
- 不能遍历,方法同get,set,has,delete。
(1)Set本身是一种构造函数,用来生成Set数据结构:new Set([iterable]),Set 对象允许你储存任何类型的唯一值,无论是原始值或者是对象引用。举个例子:
const s = new Set()
[1,2,3,4,3,2,1].forEach(x=>s.add(x))
for(let i of s) {
console.log(i) // 1 2 3 4
}
// 可以用于去掉数组中重复的对象
let arr = [1,2,3,2,1,1]
[... new Set(arr)] // [1,2,3]
向 Set 加入值的时候,不会发生类型转换,所以5
和"5"
是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===
),主要的区别是:NaN
等于自身,而精确相等运算符认为NaN
不等于自身:
let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set // set(1) {NaN}
let set1 = new Set();
set1.add(5);
set1.add('5');
console.log([...set1]) // [5,'5']
Set 实例属性:
-
constructor: 构造函数
-
size:元素数量 // 注意set.length的结果是undefined,要使用set.size计算set的长度
Set 实例方法:
操作方法
-
add(value):新增,相当于 array里的push
-
delete(value):存在即删除集合中value
-
has(value):判断集合中是否存在 value
-
clear():清空集合
Array.from方法可以将Set结构转为数组:
const items = new Set([1,2,3,2])
const array = Array.from(items)
console.log(arrar) // [1,2,3]
// 或者像下面这样使用
const arr = [..items]
console.log(arr) // [1,2,3]
遍历方法(遍历顺序为插入顺序)
-
keys():返回一个包含集合中所有键的迭代器
-
values():返回一个包含集合中所有值得迭代器
-
entries():返回一个包含Set对象中所有元素得键值对迭代器
-
forEach(callbackFn, thisArg):用于对集合成员执行callbackFn操作,如果提供了 thisArg 参数,回调中的this会是这个参数,没有返回值
let set = new Set([1, 2, 3])
console.log(set.keys()) // SetIterator {1, 2, 3}
console.log(set.values()) // SetIterator {1, 2, 3}
console.log(set.entries()) // SetIterator {1 => 1, 2 => 2, 3 => 3}
for (let item of set.keys()) {
console.log(item);
} // 1 2 3
for (let item of set.entries()) {
console.log(item);
} // [1, 1] [2, 2] [3, 3]
set.forEach((value, key) => {
console.log(key + ' : ' + value)
}) // 1 : 1 2 : 2 3 : 3
console.log([...set]) // [1, 2, 3]
Set 可默认遍历,默认迭代器生成函数是 values() 方法:
Set.prototype[Symbol.iterator] === Set.prototype.values // true
所以, Set可以使用 map、filter 方法:
let set = new Set([1,2,3])
set = new Set([...set].map(item => item*2))
console.log([...set]) // [2,4,6]
set = new Set([...set].filter(item => (item>=4)))
console.log([...set]) // [4,6]
因此,Set 很容易实现交集(Intersect)、并集(Union)、差集(Difference):
let set1 = new Set([1, 2, 3])
let set2 = new Set([4, 3, 2])
let intersect = new Set([...set1].filter(value => set2.has(value))) // 交集
let union = new Set([...set1, ...set2]) // 并集
let difference = new Set([...set1].filter(value => !set2.has(value))) // 差集
console.log(intersect) // Set {2,3}
console.log(union) // Set {1,2,3,4}
console.log(difference) // Set {1}
(2)WeakSet对象允许你将弱引用对象储存在一个集合中:
WeakSet属性:
-
constructor: 构造函数,任何一个具有 Iterable 接口的对象,都可以作参数
const arr = [[1,2],[3,4]]
const weakSet = new Weakset(arr)
console.log(weakSet) // WeakSet {[1,2],[3,4]}
WeakSet方法:
操作方法
- add(value):在WeakSet 对象中添加一个元素value;
- has(value):判断 WeakSet 对象中是否包含value;
- delete(value):删除元素 value;
- clear():清空所有元素,注意该方法已废弃。
var ws = new WeakSet()
var obj = {}
var foo = {}
ws.add(window)
ws.add(obj)
ws.has(window) // true
ws.has(foo) // false
ws.delete(window)
ws.has(window) // false
(3)Map是一种叫做字典的数据结构,以键值对的形式存储数据:
const m = new Map()
const o = {p: 'haha'}
m.set(o,'content')
m.get(o) // content
m.has(o) // true
m.delete(o) // true
m.has(o) // false
任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作Map
构造函数的参数,例如:
const set = new Set([ // 对于set来说是[value,value]
['foo',1],
['bar',2]
]);
const m1 = new Map(set); // 对于map来说是[key,value]
m1.get('foo') // 1
const m2 = new Map(['baz',3]);
const m3 = new Map(m2);
m3.get('baz') // 3
如果读取一个未知的键,则返回undefined
。
注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心:
const map = new Map();
map.set(['a'],555);
map.get(['a']) // undefined
上面代码的set
和get
方法,表面是针对同一个键,但实际上这是两个值,内存地址是不一样的,因此get
方法无法读取该键,返回undefined,
由此可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。
如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0
和-0
就是一个键,布尔值true
和字符串true
则是两个不同的键。另外,undefined
和null
也是两个不同的键。虽然NaN
不严格相等于自身,但 Map 将其视为同一个键。
let map = new Map();
map.set(-0, 123);
map.get(+0) // 123
map.set(true, 1);
map.set('true', 2);
map.get(true) // 1
map.set(undefined, 3);
map.set(null, 4);
map.get(undefined) // 3
map.set(NaN, 123);
map.get(NaN) // 123
Map属性:
-
constructor:构造函数
-
size:返回字典中所包含的元素个数
Map方法:
操作方法:
- set(key, value):向字典中添加新元素;
- get(key):通过键查找特定的数值并返回;
- has(key):判断字典中是否存在键key;
- delete(key):通过键 key 从字典中移除对应的数据;
- clear():将这个字典中的所有元素删除。
遍历方法
- Keys():将字典中包含的所有键名以迭代器形式返回;
- values():将字典中包含的所有数值以迭代器形式返回;
- entries():返回所有成员的迭代器;
- forEach():遍历字典的所有成员。
const map = new Map([
['name','An'],
['des','JS']
]);
console.log(map.entries()) // MapIterator {"name" => "An","des" => "JS"}
console.log(map.keys()) // MapIterator {"name","des"}
Map 结构的默认遍历器接口(Symbol.iterator
属性),就是entries
方法。
map[Symbol.iterator] === map.entries // true
Map 结构转为数组结构,比较快速的方法是使用扩展运算符(...
)。
对于forEach,我们先来回顾一下它的语法,第一个参数是必须的,表示数组中每个元素要调用的函数,currentValue是必须的,表示当前元素;index表示元素索引值,可选;arr表示当前元素所属的数组对象,可选。thisValue可选,传递给函数的值一般用 "this" 值,如果这个参数为空, "undefined" 会传递给 "this" 值:
array.forEach(function(currentValue, index, arr), thisValue)
看一个例子:
const reporter = {
report: function(key,value) {
console.log("Key:%s, Value:%s", key, value);
}
};
let map = new Map([
['name','An'],
['des','JS']
])
map.forEach(function(value, key, map) {
this.report(key,value);
},reporter);
// Key:name, Value:An
// Key:des, Value
在这个例子中, forEach 方法的回调函数的 this,就指向 reporter。
Map与其他数据结构的相互转换:
- Map转Array:
-
const map = new Map([[1,1],[2,2],[3,3]]) console.log([...map]) // [[1,1],[2,2],[3,3]]
- Array转Map:
-
let arr = [ {type:'1',name:'name1'}, {type:'2',name:'name2'} ]; let map = new Map(arr.map(i =>[i.type,i]));
- Map转Object:因为 Object 的键名都为字符串,而Map 的键名为对象,所以转换的时候会把非字符串键名转换为字符串键名:
-
function mapToObj(map) { let obj = Object.create(null) for (let [key,value] of map) { obj[key] = value } return obj } const map = new.Map().set('name','An').set('des','JS') mapToObj(map) // {name:'An',des:'JS'}
- Object转Map:
-
function objToMap(obj) { let map = new Map() for (let key of Object.keys(obj)) { map.set(key,obj[key]) // map,set()遍历赋值 } return map } objToMap({'name':'An','des':'JS'}) // Map {'name' => 'An','des' => 'JS'}
- Map转JSON:
-
function mapToJson(map) { return JSON.stringify([...map]) // map先转换成array数组类型,再用JSON.stringify()方法将 JavaScript 值转换为 JSON 字符串 } let map = new Map().set('name', 'An').set('des', 'JS') mapToJson(map) // [["name","An"],["des","JS"]]
- JSON转Map:
-
function jsonToMap(jsonStr) { return objToMap(JSON.parse(jsonStr)); // 先把json数据用JSON.parse()转换成object数据,再用object转map的方法 } jsonToMap('{"name": "An", "des": "JS"}') // Map {"name" => "An", "des" => "JS"}
(4)WeakMap对象是一组键值对的集合,其中的键是弱引用对象,而值可以是任意:
注意,WeakMap 弱引用的只是键名(在没有其他引用和该键引用同一对象,这个对象将会被垃圾回收(相应的key则变成无效的),所以,WeakMap 的 key 是不可枚举的),而不是键值。键值依然是正常引用。
WeakMap属性:
-
constructor:构造函数
WeakMap方法:
操作方法:
- set(key):设置一组key关联对象;
- get(key):通过键查找特定的数值并返回(没有则返回 undefined);
- has(key):判断是否有 key 关联对象;
- delete(key):通过键 key 从字典中移除对应的数据。
let myElememt = document.getElementById('logo');
let myWeakmap = new WeakMap();
nyWeakmap.set(myElement, {timesClicked: 0});
myElement.addWventListener('click', function() {
let logoData = myWeakmap.get(Element);
logoData.timesClicked ++;
}, false)
5、ES5/ES6 的继承除了写法以外还有什么区别?
答案:在ES6中,子类可以直接通过 __proto__ 寻址到父类:
class Super {}
class Sub extends Super {}
const sub = new Sub();
sub._proto_ === Super; // true
而通过 ES5 的方式,Sub.__proto__ === Function.prototype:
function Super() {}
function Sub() {}
Sub.propotype = new Super();
Sub.prototype.constructor = Sub;
var sub = new Sub();
Sub._proto_ === Function.prototype; // true
ES5 和 ES6 子类this生成顺序不同:
ES5 的继承先生成了子类实例,再调用父类的构造函数修饰子类实例;ES6 的继承先生成父类实例,再调用子类的构造函数修饰父类实例。这个差别使得 ES6 可以继承内置对象。
6、setTimeout、Promise、Async/Await 的区别?
答案:
先大致讲述一下三者的语法用法等:
setTimeout:
语法:其中code/function必须;milliseconds可选,表示等待的时间,以毫秒计,默认为0;param1, param2,...可选,表示传给执行函数的其他参数:
setTimeout(code, milliseconds, param1, param2,...)
setTimeout(function, milliseconds, param1, param2,...)
如果你只想重复执行可以使用 setInterval() 方法。使用 clearTimeout() 方法来阻止函数的执行。
Promise:Promise 对象代表了未来将要发生的事件,new Promise
在实例化的过程中所执行的代码都是同步进行的,而then
中注册的回调才是异步执行的。在同步代码执行完成后才回去检查是否有异步任务完成,并执行对应的回调:
语法:Promise 构造函数包含一个参数和一个带有 resolve(解析)和 reject(拒绝)两个参数的回调。在回调中执行一些操作(例如异步),如果一切都正常,则调用 resolve,否则调用 reject:
var promise = new Promise(function(resolve, reject) {
// 异步处理
// 处理结束后、调用resolve 或 reject
});
对于已经实例化过的 promise 对象可以调用 promise.then() 方法,传递 resolve 和 reject 方法作为回调。
promise.then() 是 promise 最为常用的方法:
promise.then(onFulfilled, onRejected)
// promise简化了对error的处理,上面的代码我们也可以这样写
promise.then(onFulfilled).catch(onRejected))
对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:
- pending: 初始状态,不是成功或失败状态;
- fulfilled: 意味着操作成功完成;
- rejected: 意味着操作失败。
Promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行,一个Promise实例:
console.log('script start')
let promise1 = new Promise(function(resolve) { // promise异步属于微任务
console.log('promise1')
resolve()
console.log('promise1 end')
}).then(function() {
console.log('promise2')
})
setTimeout(function() { // settimeout属于宏任务
console.log('settimeout')
})
console.log('script end')
// 输出顺序:
// script start
// promise1
// promise1 end
// script end
// promise2
// settimeout
// js线程处理完,才会去执行定时器里的东西,异步的代码肯定是在同步的后面执行,所以最后执行的是settimeout和then里面的东西
// 微任务总会在下一个宏任务之前全部执行完毕,所以一般是先执行then,结束之后执行下一个宏任务也就是settimeout
// 宏任务都有哪些:I/O, setTimeout, setInterval, setImmediate, requestAnimationFrame
// 微任务都有哪些:process.nextTick, MutationObserver, Promise.then catch finally
当JS主线程执行到Promise对象时,
-
promise1.then() 的回调就是一个任务;
-
promise1 是 resolved或rejected: 那这个任务就会放入当前事件循环回合的微任务队列;
-
promise1 是 pending: 这个任务就会放入事件循环的未来的某个(可能下一个)回合的微任务队列中;
-
setTimeout 的回调也是个任务 ,它会被放入宏任务队列即使是 0ms 的情况。
Async:也是用来处理异步的,async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体:
语法:name声明函数名称;param是要传递给函数的参数的名称;statements是函数体的语句:
async function name([param[,param[,...param]]]) {statements}
async function async1(){
console.log('async1 start');
await async2();
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start');
async1();
console.log('script end')
// // 输出顺序:script start->async1 start->async2->script end->async1 end
async函数返回一个Promise对象,可以使用then方法添加回调函数:
async function helloAsync() {
return "helloAsync";
}
console.log(helloAsync()) // Promise {<resolved>: "helloAsync"}
helloAsync().then(v => {
console.log(v); // helloAsync
})
await 关键字仅在 async function 中有效。如果在 async function 函数体外使用 await ,你只会得到一个语法错误:
function testAwait() {
return new Promise(resolve) =>{
setTimeout(function() {
console.log("testAwait");
resolve();
}, 1000);
});
}
async function helloAsync() {
await testAwait(); // 遇到await会先暂停执行,等触发的异步操作完成后再恢复await函数的执行
console.log("helloAsync");
}
helloAsync();
// testAwait
// helloAsync
下面简单的讲一下三者的区别:
这题主要是考察三者在事件循环中的区别,时间循环分为宏任务队列和维任务队列:
- setTimeout的回调函数放到宏任务队列里,等到执行栈清空以后执行;
- promise.then里的回调函数会放到相应宏任务的微任务队列里,等宏任务里面的同步代码执行完再执行;
- async函数表示函数里面可能会有异步方法,await后面跟一个表达式,async方法执行时,遇到await会立即执行表达式,然后把表达式后面的代码放到微任务队列里,让出执行栈让同步代码先执行。
7、异步笔试题:请写出下面代码的运行结果:
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
答案:script start——async1 start——async2——promise1——script end——async1 end——promise2——setTimeout
8、(携程)算法手写题:已知如下数组:
var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];
编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组:
答案:
Array.from(new Set(arr.flat(Infinity))).sort((a,b)=>{ return a-b})
flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回,使用 Infinity 作为深度,展开任意深度的嵌套数组;
sort() 方法用于对数组的元素进行排序;
ES6 提供了新的数据结构 Set:它类似于数组,但是成员的值都是唯一的,没有重复的值。Set 本身是一个构造函数,用来生成 Set 数据结构。Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
或者:使用arr.toString().split(",")进行扁平化,这时数组元素都变成了字符串;最后一步map() 用来将数组再变成数字,不然数组元素都是字符串:
var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10]
// 扁平化
var flatArr = arr.toString().split(",");
// 去重
let disArr = Array.from(new Set(flatArr));
// 排序
let result = disArr.sort(function(a, b) {
return a-b
})
console.log(result.map(Number))
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
9、(滴滴、挖财、微医、海康)JS 异步解决方案的发展历程以及优缺点:
答案:
Javascript语言的执行环境是"单线程"(single thread)。
所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。
为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。
"同步模式"就是上一段的模式,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;"异步模式"则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。
"异步模式"非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。
异步模式编程的四种方法:
- 回调函数:假定两个函数,f1和f2,后者等待前者的执行结果:
f1(); f2();
如果f1很耗时,那么可以考虑改写f1,把f2写成f1的回调函数:
function f1(callback) { setTimeout(function() { // f1的任务代码 callback(); }, 1000) }
执行代码就变成下面这样:
f1(f2);
采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,回调地狱,不能使用try catch捕获错误,不能return,各个部分之间高度耦合(Coupling),流程会很混乱,而且每个任务只能指定一个回调函数。
-
事件监听:任务的执行不取决于代码的顺序,而取决于某个事件是否发生:还是以f1和f2为例。首先,为f1绑定一个事件(这里采用的jQuery的写法)
f1.on("done",f2);
这行代码的意思是,当f1发生done事件,就执行f2,然后,我们对f1进行改写:
function f1() { setTimeout(function() { // f1的任务代码 f1.trigger("done"); }, 1000); }
f1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行f2。这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合"(Decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。
-
发布/订阅:
我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。
这个模式有多种实现,下面采用的是Ben Alman的Tiny Pub/Sub,这是jQuery的一个插件。
首先,f2向"信号中心"jQuery订阅"done"信号:
jQuery.subscribe("done",f2);
然后,f1进行如下改写:
function f1() { setTimeout(function() { // f1的任务代码 jQuery.publish("done"); },1000); }
jQuery.publish("done")的意思是,f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引发f2的执行。
此外,f2完成执行后,也可以取消订阅(unsubscribe):
jQuery.unsubscribe("done",f2);
这种方法的性质与"事件监听"类似,但是明显优于后者。因为我们可以通过查看"消息中心",了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
-
Promises对象:
Promises对象是CommonJS工作组提出的一种规范,目的是为异步编程提供统一接口。
简单说,它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。比如,f1的回调函数f2,可以写成:
f1().then(f2);
f1要进行如下改写(这里使用的是jQuery的实现):
function f1() { var dfd = $.Deferred(); setTimeout(function() { // f1的任务代码 dfd.resolve(); },500); return dfd.promise; // 返回一个Deferred的Promise对象 }
这样写的优点在于,回调函数变成了链式写法,程序的流程可以看得很清楚,而且有一整套的配套方法,可以实现许多强大的功能。比如指定多个回调函数:
f1().then(f2).then(f3);
再比如,指定发生错误时的回调函数:
f1().then(f2),fail(f3);
而且,它还有一个前面三种方法都没有的好处:如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。所以,你不用担心是否错过了某个事件或信号。这种方法的缺点就是编写和理解都相对比较难,无法取消Promise,错误需要通过回调函数来捕获。
9、如何实现一个 new?
答案:首先,new运算符做了两件事:
- 创建了一个全新的对象;
- 这个对象会被执行
[[Prototype]]
(也就是_proto_
)链接; - 生成的新对象会绑定到函数调用的this;
- 如果函数没有返回对象类型
Object
(包含Functoin
,Array
,Date
,RegExg
,Error
),那么new
表达式中的函数调用会自动返回这个新的对象。
// 实现一个new
var Dog = function(name) {
this.name = name
}
Dog.prototype.bark = function() {
console.log('wangwang')
}
Dog.prototype.sayName = function() {
console.log('my name is ' + this.name)
}
let sanmao = new Dog('三毛')
sanmao.sayName()
sanmao.bark()
// new的实现:
function _new(fn, ...arg){
const obj = Object.create(fn.prototype); // Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的proto。fn.prototype是新建对象的原型对象,即该参数会被赋值到目标对象的原型上
const ret = fn.apply(obj, arg);
return ret instanceof Object ? ret:obj;
}
var simao = _new(Dog, 'simao')
simao.bark()
simao.sayName()
console.log(simao instanceof Dog)
// true
10、简单讲解一下http2的多路复用?
答案:
在 HTTP1 中,每次请求都会建立一次HTTP连接,也就是我们常说的3次握手4次挥手,这个过程在一次请求过程中占用了相当长的时间,即使开启了 Keep-Alive ,解决了多次连接的问题,但是依然有两个效率上的问题:
- 第一个:串行的文件传输。当请求a文件时,b文件只能等待,等待a连接到服务器、服务器处理文件、服务器返回文件,这三个步骤。我们假设这三步用时都是1秒,那么a文件用时为3秒,b文件传输完成用时为6秒,依此类推。(注:此项计算有一个前提条件,就是浏览器和服务器是单通道传输)
- 第二个:连接数过多。我们假设Apache设置了最大并发数为300,因为浏览器限制,浏览器发起的最大请求数为6,也就是服务器能承载的最高并发为50,当第51个人访问时,就需要等待前面某个请求处理完成。
HTTP2的多路复用就是为了解决上述的两个性能问题。
在 HTTP2 中,有两个非常重要的概念,分别是帧(frame)和流(stream)。
帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。
多路复用,就是在一个 TCP 连接中可以存在多条流。换句话说,也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能。
简单来说, 就是在同一个TCP连接,同一时刻可以传输多个HTTP请求。
11、介绍下 npm 模块安装机制,为什么输入 npm install 就可以自动安装对应的模块?
答案:
npm模块安装机制:
- 发出npm install命令;
- 查询node_modules目录中是否已经存在指定模块;
- 若存在,不再重新安装,若不存在:
- npm向registry查询模块压缩包的网址;
- 下载压缩包,存放在根目录下的.npm目录中;
- 解压压缩包到当前项目的node_modules目录里。
npm实现原理:
输入npm install命令并敲下回车后,会经历以下几个阶段(以npm 5.5.1为例):
1、执行工程自身preinstall:
当前npm工程如果定义了preinstall钩子,此时会被执行;
2、确定首层依赖模块:
首先需要做的是确定工程中的首层依赖,也就是 dependencies 和 devDependencies 属性中直接指定的模块(假设此时没有添加 npm install 参数)。
工程本身是整棵依赖树的根节点,每个首层依赖模块都是根节点下面的一棵子树,npm 会开启多进程从每个首层依赖模块开始逐步寻找更深层级的节点;
3、获取模块:
获取模块是一个递归的过程,分为以下几步:
- 获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为 package.json 中往往是 semantic version(semver,语义化版本)。此时如果版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有该模块信息直接拿即可,如果没有则从仓库获取。如 packaeg.json 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合 1.x.x 形式的最新版本;
- 获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载;
- 查找该模块依赖,如果有依赖则回到第1步,如果没有则停止。
4、模块扁平化(dedupe):
上一步获取到的是一棵完整的依赖树,其中可能包含大量重复模块。比如 A 模块依赖于 loadsh,B 模块同样依赖于 lodash。在 npm3 以前会严格按照依赖树的结构进行安装,因此会造成模块冗余——从 npm3 开始默认加入了一个 dedupe 的过程。它会遍历所有节点,逐个将模块放在根节点下面,也就是 node-modules 的第一层。当发现有重复模块时,则将其丢弃。
这里需要对重复模块进行一个定义,它指的是模块名相同且 semver 兼容。每个 semver 都对应一段版本允许范围,如果两个模块的版本允许范围存在交集,那么就可以得到一个兼容版本,而不必版本号完全一致,这可以使更多冗余模块在 dedupe 过程中被去掉:
比如 node-modules 下 foo 模块依赖 lodash@^1.0.0,bar 模块依赖 lodash@^1.1.0,则 ^1.1.0 为兼容版本;
而当 foo 依赖 lodash@^2.0.0,bar 依赖 lodash@^1.1.0,则依据 semver 的规则,二者不存在兼容版本。会将一个版本放在 node_modules 中,另一个仍保留在依赖树里;
5、安装模块:
这一步将会更新工程中的 node_modules,并执行模块中的生命周期函数(按照 preinstall、install、postinstall 的顺序);
6、执行工程自身生命周期:
当前 npm 工程如果定义了钩子,此时会被执行(按照 install、postinstall、prepublish、prepare 的顺序),最后一步是生成或更新版本描述文件,npm install 过程完成。
12、有以下 3 个判断数组的方法,请分别介绍它们之间的区别和优劣:Object.prototype.toString.call()、instanceof以及Array.isArray()
答案:
Object.prototype.toString.call():
每一个继承Object的对象都有toString方法,如果 toString 方法没有重写的话,会返回[Object type],其中 type 为对象的类型。但当除了Object类型的对象外,其他类型直接使用 toString 方法时,会直接返回都是内容的字符串,所以我们需要使用call或者apply方法来改变toString方法的执行上下文:
const an = ['Hello', 'An'];
an.toString(); // "Hello,An"
Object.prototype.toString.call(an); // "[Object Array]"
这种方法对于所有基本的数据类型都能进行判断,即使是 null 和 undefined :
Object.prototype.toString.call('An'); // '[Object Array]'
Object.prototype.toString.call(1); // '[Object Number]'
Object.prototype.toString.call(Symbol(1)); // '[Object Symbol]'
Object.prototype.toString.call(null); // '[Object Null]'
Object.prototype.toString.call(undefined); // '[Object Undefined]'
Object.prototype.toString.call('An'); // '[Object Array]'
Object.prototype.toString.call(function() {}); // '[Object Function]'
Object.prototype.toString.call({name: 'An'}); // '[Object Onbject]'
Object.prototype.toString.call()
常用于判断浏览器内置对象时。
instanceof:
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。使用 instanceof
判断一个对象是否为数组,instanceof
会判断这个对象的原型链上是否会找到对应的 Array
的原型,找到返回 true
,否则返回 false:
[] instanceof Array; // true
但 instanceof
只能用来判断对象类型,原始类型不可以,并且所有对象类型 instanceof Object 都是 true:
[] instanceof Object; // true
Array.isArray:
Array.isArray用来判断对象是否为数组。
- 其中instanceof 与 isArray的区别在于:当检测Array实例时,
Array.isArray
优于instanceof
,因为Array.isArray
可以检测出iframes:
var iframe = document.createElement('iframe'); document.body.appendChild(iframe); xArray = window.frames[window.frames.length-1].Array; var arr = new xArray(1,2,3); // [1,2,3] // 以下两种方法都能够正确检测出是数组 Array.isArray(arr); // true Object.prototype.toString.call(arr); // "[object Array]" // 在框架中不起作用,无法正确检测出数组 arr instanceof Array; // false
13、介绍一下重绘和回流(Repaint & Reflow),以及如何优化?
答案:
重绘:
由于节点的几何属性发生改变或者由于样式发生改变而不会影响布局的,称为重绘,例如outline
, visibility
, color
、background-color
等,重绘的代价是高昂的,因为浏览器必须验证DOM树上其他节点元素的可见性。
回流:
回流是布局或者几何属性需要改变就称为回流。回流是影响浏览器性能的关键因素,因为其变化涉及到部分页面(或是整个页面)的布局更新。一个元素的回流可能会导致了其所有子元素以及DOM中紧随其后的节点、祖先节点元素的随后的回流。例如:
<body>
<div class="error">
<h4>我的组件</h4>
<p><strong>错误:</strong>错误的描述…</p>
<h5>错误纠正</h5>
<ol>
<li>第一步</li>
<li>第二步</li>
</ol>
</div>
</body>
在上面的HTML片段中,对该段落(<p>
标签)回流将会引发强烈的回流,因为它是一个子节点。这也导致了祖先的回流(div.error
和body
– 视浏览器而定)。此外,<h5>
和<ol>
也会有简单的回流,因为其在DOM中在回流元素之后。大部分的回流将导致页面的重新渲染。
回流必定会发生重绘,重绘不一定会引发回流。
浏览器优化:
现代浏览器大多都是通过队列机制来批量更新布局,浏览器会把修改操作放在队列中,至少一个浏览器刷新(即16.6ms)才会清空队列,但当你获取布局信息的时候,队列中可能有会影响这些属性或方法返回值的操作,即使没有,浏览器也会强制清空队列,触发回流与重绘来确保返回正确的值。
主要包括以下属性或方法:
offsetTop
、offsetLeft
、offsetWidth
、offsetHeight
scrollTop
、scrollLeft
、scrollWidth
、scrollHeight
clientTop
、clientLeft
、clientWidth
、clientHeight
width
、height
getComputedStyle()
getBoundingClientRect()
所以,我们应该避免频繁的使用上述的属性,他们都会强制渲染刷新队列。
减少重绘与回流:
1、CSS:
- 使用transform替代top;
- 使用 visibility 替换display: none,因为前者只会引起重绘,后者则会引发回流(改变了布局);
- 避免使用table布局,可能很小的改动都会造成整个 table 的重新布局;
- 尽可能在
DOM
树的最末端改变class
,回流是不可避免的,但可以减少其影响。尽可能在DOM树的最末端改变class,可以限制了回流的范围,使其影响尽可能少的节点; - 避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多:
<div> <a> <span></span> </a> </div> <style> span { color: red; } div > a > span { color: red; } </style>
对于第一种设置样式的方式来说,浏览器只需要找到页面中所有的
span
标签然后设置颜色,但是对于第二种设置样式的方式来说,浏览器首先需要找到所有的span
标签,然后找到span
标签上的a
标签,最后再去找到div
标签,然后给符合这种条件的span
标签设置颜色,这样的递归过程就很复杂。所以我们应该尽可能的避免写过于具体的 CSS 选择器,然后对于 HTML 来说也尽量少的添加无意义标签,保证层级扁平; -
将动画效果应用到
position
属性为absolute
或fixed
的元素上,避免影响其他元素的布局,这样只是一个重绘,而不是回流,同时,控制动画速度可以选择requestAnimationFrame
,详见探讨 requestAnimationFrame; -
避免使用CSS表达式,可能会引发回流;
-
将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点,例如
will-change
、video
、iframe
等标签,浏览器会自动将该节点变为图层; -
CSS3 硬件加速(GPU加速),使用css3硬件加速,可以让
transform
、opacity
、filters
这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color
这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。
2、JavaScript:
- 避免频繁操作样式,最好一次性重写
style
属性,或者将样式列表定义为class
并一次性更改class
属性; - 避免频繁操作DOM,创建一个
documentFragment
,在它上面应用所有DOM操作
,最后再把它添加到文档中; - 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来;
- 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。