1.说一说JS异步发展史
异步最早的解决方案是回调函数,如事件的回调,setInterval/setTimeout中的回调。但是回调函数有一个很常见的问题,就是回调地狱的问题(稍后会举例说明);
为了解决回调地狱的问题,社区提出了Promise解决方案,ES6将其写进了语言标准。Promise解决了回调地狱的问题,但是Promise也存在一些问题,如错误不能被try catch,而且使用Promise的链式调用,其实并没有从根本上解决回调地狱的问题,只是换了一种写法。
ES6中引入 Generator 函数,Generator是一种异步编程解决方案,Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权,Generator 函数可以看出是异步任务的容器,需要暂停的地方,都用yield语句注明。但是 Generator 使用起来较为复杂。
ES7又提出了新的异步解决方案:async/await,async是 Generator 函数的语法糖,async/await 使得异步代码看起来像同步代码,异步编程发展的目标就是让异步逻辑的代码看起来像同步一样。
1)回调函数:call back
//node读取文件
fs.readFile(xxx, 'utf-8', function(err, data) {
//code
});
回调函数的使用场景(包括但不限于):
1. 事件回调
2. Node API
3. setTimeout/setInterval中的回调函数
异步回调嵌套会导致代码难以维护,并且不方便统一处理错误,不能try catch 和 回调地狱(如先读取A文本内容,再根据A文本内容读取B再根据B的内容读取C...)。
fs.readFile(A, 'utf-8', function(err, data) {
fs.readFile(B, 'utf-8', function(err, data) {
fs.readFile(C, 'utf-8', function(err, data) {
fs.readFile(D, 'utf-8', function(err, data) {
//....
});
});
});
});
2)Promise
Promise 主要解决了回调地狱的问题,Promise 最早由社区提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。
那么我们看看Promise是如何解决回调地狱问题的,仍以上文的readFile为例。
function read(url) {
return new Promise((resolve, reject) => {
fs.readFile(url, 'utf8', (err, data) => {
if(err) reject(err);
resolve(data);
});
});
}
read(A).then(data => {
return read(B);
}).then(data => {
return read(C);
}).then(data => {
return read(D);
}).catch(reason => {
console.log(reason);
});
运行代码效果
const fs = require('fs');
/**
* 将 fs.readFile 包装成promise接口
*/
function read(url) {
return new Promise((resolve, reject) => {
fs.readFile(url, 'utf8', (err, data) => {
if(err) reject(err);
resolve(data);
});
});
}
/**
* 解决回调地狱
* 注: code Runner 的bug导致,相对路径是从根目录开始
*/
read('./JS/Async/data/info.txt').then((data) => {
return read(data);
}).then((data) => {
return read(data);
}).then((data) => {
console.log(data); //22
}).catch((err) => {
//可以对错误进行统一处理
console.log(err);
});
思考一下在Promise之前,你是如何处理异步并发问题的,假设有这样一个需求:读取三个文件内容,都读取成功后,输出最终的结果。有了Promise之后,又如何处理呢?
代码:
const fs = require('fs');
/**
* Promise 之前
*
* 如果使用回调函数实现多个异步并行执行,同一时刻获取最终结果
* 可以借助 发布订阅/观察者模式
*/
let pubsub = {
arry: [],
emit() {
this.arry.forEach(fn => fn());
},
on(fn) {
this.arry.push(fn);
}
}
let data = [];
pubsub.on(() => {
if(data.length === 3) {
console.log(data); //[ '22', 'Yvette', 'engineer' ];数组顺序随机
}
});
fs.readFile('./JS/Async/data/age.txt', 'utf-8', (err, value) => {
data.push(value);
pubsub.emit();
});
fs.readFile('./JS/Async/data/name.txt', 'utf-8', (err, value) => {
data.push(value);
pubsub.emit();
});
fs.readFile('./JS/Async/data/job.txt', 'utf-8', (err, value) => {
data.push(value);
pubsub.emit();
});
/**
* 将 fs.readFile 包装成promise接口
*/
function read(url) {
return new Promise((resolve, reject) => {
fs.readFile(url, 'utf8', (err, data) => {
if(err) reject(err);
resolve(data);
});
});
}
/**
* 使用 Promise
*
* 通过 Promise.all 可以实现多个异步并行执行,同一时刻获取最终结果的问题
*/
Promise.all([
read('./JS/Async/data/age.txt'),
read('./JS/Async/data/name.txt'),
read('./JS/Async/data/job.txt')
]).then(data => {
console.log(data); //[ '22', 'Yvette', 'engineer' ];数组顺序随机
}).catch(err => console.log(err));
/**
* 使用 Async/Await
*/
async function readAsync() {
let data = await Promise.all([
read('./JS/Async/data/age.txt'),
read('./JS/Async/data/name.txt'),
read('./JS/Async/data/job.txt')
]);
return data;
}
readAsync().then(data => {
console.log(data); //[ '22', 'Yvette', 'engineer' ];数组顺序随机
});
注: 可以使用 bluebird 将接口 promise化;
引申: Promise有哪些优点和问题呢?
优点:
1. 一旦状态改变,就不会再变,任何时候都可以得到这个结果
2. 可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数
缺点:
1. 无法取消 Promise
2. 当处于pending状态时,无法得知目前进展到哪一个阶段
3. 错误不能被try catch -->
3)Generator
Generator 函数是 ES6 提供的一种异步编程解决方案,整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。
Generator 函数一般配合 yield 或 Promise 使用。Generator函数返回的是迭代器。对生成器和迭代器不了解的同学,请自行补习下基础。下面我们看一下 Generator 的简单使用:
function* gen() {
let a = yield 111;
console.log(a);
let b = yield 222;
console.log(b);
let c = yield 333;
console.log(c);
let d = yield 444;
console.log(d);
}
let t = gen();
//next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值
t.next(1); //第一次调用next函数时,传递的参数无效
t.next(2); //a输出2;
t.next(3); //b输出3;
t.next(4); //c输出4;
t.next(5); //d输出5;
为了让大家更好的理解上面代码是如何执行的,下图,分别对应每一次的next方法调用:
仍然以上文的readFile为例,使用 Generator + co库来实现:
const fs = require('fs');
const co = require('co');
const bluebird = require('bluebird');
const readFile = bluebird.promisify(fs.readFile);
function* read() {
yield readFile(A, 'utf-8');
yield readFile(B, 'utf-8');
yield readFile(C, 'utf-8');
//....
}
co(read()).then(data => {
//code
}).catch(err => {
//code
});
不使用co库,如何实现?能否自己写一个最简的my_co?
const fs = require('fs');
const bluebird = require('bluebird');
const readFile = bluebird.promisify(fs.readFile);
/**
* 读取A--->读取B--->读取C
*/
function* read() {
let info = yield readFile('./JS/Async/data/info.txt', 'utf-8');
let base = yield readFile(info, 'utf-8');
let age = yield readFile(base, 'utf-8');
return age;
}
let it = read();
let { value, done } = it.next();
value.then((data) => {
let { value, done } = it.next(data); //data赋值给了 info
value.then((data) => {
let { value, done } = it.next(data); //data赋值给了 base
value.then((data) => {
let { value, done } = it.next(data); //data赋值给base
console.log(value); //输出22
});
});
});
// 引入co
const co = require('co');
co(read()).then(data => {
console.log(data); //输出22
}).catch(err => {
console.log(err);
});
/**
* 自己实现一个 co
* 接受一个迭代器
*/
function my_co (it) {
return new Promise((resolve, reject) => {
function next(data) {
let {value, done} = it.next(data);
if(!done) {
value.then(val => {
next(val);
}, reject);
}else{
resolve(value);
}
}
next();
});
}
my_co(read()).then(data => {
console.log(data); //输出22
});
PS: 如果你还不太了解 Generator/yield,建议阅读ES6相关文档。
4)async/await
ES7中引入了 async/await 概念。async其实是一个语法糖,它的实现就是将Generator函数和自动执行器(co),包装在一个函数中。
async/await 的优点是代码清晰,不用像 Promise 写很多 then 链,就可以处理回调地狱的问题。错误可以被try catch。
仍然以上文的readFile为例,使用 Generator + co库来实现:
const fs = require('fs');
const bluebird = require('bluebird');
const readFile = bluebird.promisify(fs.readFile);
async function read() {
await readFile(A, 'utf-8');
await readFile(B, 'utf-8');
await readFile(C, 'utf-8');
//code
}
read().then((data) => {
//code
}).catch(err => {
//code
});
思考一下 async/await 如何处理异步并发问题的?
2.谈谈对 async/await 的理解,async/await 的实现原理是什么?
async/await 就是 Generator 的语法糖,使得异步操作变得更加方便。下图对比一下分析:
async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成await。
我们说 async 是 Generator 的语法糖,那么这个糖究竟甜在哪呢?
1)async函数内置执行器,函数调用之后,会自动执行,输出最后结果。而Generator需要调用next或者配合co模块使用。
2)更好的语义,async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
3)更广的适用性。co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async 函数的 await 命令后面,可以是 Promise 对象和原始类型的值。
4)返回值是Promise,async函数的返回值是 Promise 对象,Generator的返回值是 Iterator,Promise 对象使用起来更加方便。
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
具体代码试下如下(和spawn的实现略有差异),如果你想知道如何一步步写出 my_co ,可以访问: https://github.com/YvetteLau/Blog/blob/master/JS/Async/my_async.js
function my_co(it) {
return new Promise((resolve, reject) => {
function next(data) {
try {
var { value, done } = it.next(data);
}catch(e){
return reject(e);
}
if (!done) {
//done为true,表示迭代完成
//value 不一定是 Promise,可能是一个普通值。使用 Promise.resolve 进行包装。
Promise.resolve(value).then(val => {
next(val);
}, reject);
} else {
resolve(value);
}
}
next(); //执行一次next
});
}
function* test() {
yield new Promise((resolve, reject) => {
setTimeout(resolve, 100);
});
yield new Promise((resolve, reject) => {
// throw Error(1);
resolve(10)
});
yield 10;
return 1000;
}
my_co(test()).then(data => {
console.log(data); //输出1000
}).catch((err) => {
console.log('err: ', err);
});
3.使用 async/await 需要注意什么?
1. await 命令后面的Promise对象,运行结果可能是 rejected,此时等同于 async 函数返回的Promise 对象被reject。因此需要加上错误处理,可以给每个 await 后的 Promise 增加 catch 方法;也可以将 await 的代码放在 try...catch 中。
2. 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
//下面两种写法都可以同时触发
//法一
async function f1() {
await Promise.all([
new Promise((resolve) => {
setTimeout(resolve, 600);
}),
new Promise((resolve) => {
setTimeout(resolve, 600);
})
])
}
//法二
async function f2() {
let fn1 = new Promise((resolve) => {
setTimeout(resolve, 800);
});
let fn2 = new Promise((resolve) => {
setTimeout(resolve, 800);
})
await fn1;
await fn2;
}
await命令只能用在async函数之中,如果用在普通函数,会报错。
async 函数可以保留运行堆栈。
/**
* 函数a内部运行了一个异步任务b()。当b()运行的时候,函数a()不会中断,而是继续执行。
* 等到b()运行结束,可能a()早就* 运行结束了,b()所在的上下文环境已经消失了。
* 如果b()或c()报错,错误堆栈将不包括a()。
*/
function b() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 200)
});
}
function c() {
throw Error(10);
}
const a = () => {
b().then(() => c());
};
a();
/**
* 改成async函数
*/
const m = async () => {
await b();
c();
};
m();
报错信息如下,可以看出 async 函数可以保留运行堆栈。
4.如何实现 Promise.race?
在代码实现前,我们需要先了解 Promise.race 的特点:
1. Promise.race返回的仍然是一个Promise.
它的状态与第一个完成的Promise的状态相同。它可以是完成( resolves),也可以是失败(rejects),这要取决于第一个Promise是哪一种状态。
1. 如果传入的参数是不可迭代的,那么将会抛出错误。
2. 如果传的参数数组是空,那么返回的 promise 将永远等待。
3. 如果迭代包含一个或多个非承诺值和/或已解决/拒绝的承诺,则 Promise.race 将解析为迭代中找到的第一个值。
Promise.race = function (promises) {
//promises 必须是一个可遍历的数据结构,否则抛错
return new Promise((resolve, reject) => {
if (typeof promises[Symbol.iterator] !== 'function') {
//真实不是这个错误
Promise.reject('args is not iteratable!');
}
if (promises.length === 0) {
return;
} else {
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then((data) => {
resolve(data);
return;
}, (err) => {
reject(err);
return;
});
}
}
});
}
测试代码:
//一直在等待态
Promise.race([]).then((data) => {
console.log('success ', data);
}, (err) => {
console.log('err ', err);
});
//抛错
Promise.race().then((data) => {
console.log('success ', data);
}, (err) => {
console.log('err ', err);
});
Promise.race([
new Promise((resolve, reject) => { setTimeout(() => { resolve(100) }, 1000) }),
new Promise((resolve, reject) => { setTimeout(() => { resolve(200) }, 200) }),
new Promise((resolve, reject) => { setTimeout(() => { reject(100) }, 100) })
]).then((data) => {
console.log(data);
}, (err) => {
console.log(err);
});
引申: Promise.all/Promise.reject/Promise.resolve/Promise.prototype.finally/Promise.prototype.catch 的实现原理,如果还不太会,可以查看这里:https://github.com/YvetteLau/Blog/issues/2
5.可遍历数据结构的有什么特点?
一个对象如果要具备可被 for...of 循环调用的 Iterator 接口,就必须在其 Symbol.iterator 的属性上部署遍历器生成方法(或者原型链上的对象具有该方法)
PS: 遍历器对象根本特征就是具有next方法。每次调用next方法,都会返回一个代表当前成员的信息对象,具有value和done两个属性。
//如为对象添加Iterator 接口;
let obj = {
name: "Yvette",
age: 18,
job: 'engineer',
[Symbol.iterator]() {
const self = this;
const keys = Object.keys(self);
let index = 0;
return {
next() {
if (index < keys.length) {
return {
value: self[keys[index++]],
done: false
};
} else {
return { value: undefined, done: true };
}
}
};
}
};
for(let item of obj) {
console.log(item); //Yvette 18 engineer
}
使用 Generator 函数(遍历器对象生成函数)简写 Symbol.iterator 方法,可以简写如下:
let obj = {
name: "Yvette",
age: 18,
job: 'engineer',
* [Symbol.iterator] () {
const self = this;
const keys = Object.keys(self);
for (let index = 0;index < keys.length; index++) {
yield self[keys[index]];//yield表达式仅能使用在 Generator 函数中
}
}
};
原生具备 Iterator 接口的数据结构如下:
Array
Map
Set
String
TypedArray
函数的 arguments 对象
NodeList 对象
ES6 的数组、Set、Map 都部署了以下三个方法: entries() / keys() / values(),调用后都返回遍历器对象。
6.requestAnimationFrame 和 setTimeout/setInterval 有什么区别?使用 requestAnimationFrame 有哪些好处?
在 requestAnimationFrame 之前,我们主要使用 setTimeout/setInterval 来编写JS动画。
编写动画的关键是循环间隔的设置,一方面,循环间隔足够短,动画效果才能显得平滑流畅;另一方面,循环间隔还要足够长,才能确保浏览器有能力渲染产生的变化。
大部分的电脑显示器的刷新频率是60HZ,也就是每秒钟重绘60次。大多数浏览器都会对重绘操作加以限制,不超过显示器的重绘频率,因为即使超过那个频率用户体验也不会提升。因此,最平滑动画的最佳循环间隔是 1000ms / 60 ,约为16.7ms。
setTimeout/setInterval 有一个显著的缺陷在于时间是不精确的,setTimeout/setInterval 只能保证延时或间隔不小于设定的时间。因为它们实际上只是把任务添加到了任务队列中,但是如果前面的任务还没有执行完成,它们必须要等待。
requestAnimationFrame 才有的是系统时间间隔,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使用动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。
综上所述,requestAnimationFrame 和 setTimeout/setInterval 在编写动画时相比,优点如下:
1.requestAnimationFrame 不需要设置时间,采用系统时间间隔,能达到最佳的动画效果。
2.requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成。
3.当 requestAnimationFrame() 运行在后台标签页或者隐藏的
7.JS 类型转换的规则是什么?
JS中类型转换分为 强制类型转换 和 隐式类型转换 。
1. 通过 Number()、parseInt()、parseFloat()、toString()、String()、Boolean(),进行强制类型转换。
2. 逻辑运算符(&&、 ||、 !)、运算符(+、-、*、/)、关系操作符(>、 <、 <= 、>=)、相等运算符(==)或者 if/while 的条件,可能会进行隐式类型转换。
强制类型转换
1.Number() 将任意类型的参数转换为数值类型
规则如下:
如果是布尔值,true和false分别被转换为1和0
如果是数字,返回自身
如果是 null,返回 0
如果是 undefined,返回 NAN
如果是字符串,遵循以下规则:
1. 如果字符串中只包含数字(或者是 `0X` / `0x` 开头的十六进制数字字符串,允许包含正负号),则将其转换为十进制
2. 如果字符串中包含有效的浮点格式,将其转换为浮点数值
3. 如果是空字符串,将其转换为0
4. 如不是以上格式的字符串,均返回 `NaN`
如果是Symbol,抛出错误
如果是对象,则调用对象的 valueOf() 方法,然后依据前面的规则转换返回的值。如果转换的结果是 NaN ,则调用对象的 toString() 方法,再次依照前面的规则转换返回的字符串值。
部分内置对象调用默认的 valueOf 的行为:
Number('0111'); //111
Number('0X11') //17
Number(null); //0
Number(''); //0
Number('1a'); //NaN
Number(-0X11);//-17
2.parseInt(param, radix)
如果第一个参数传入的是字符串类型:
忽略字符串前面的空格,直至找到第一个非空字符,如果是空字符串,返回NaN
如果第一个字符不是数字符号或者正负号,返回NaN
如果第一个字符是数字/正负号,则继续解析直至字符串解析完毕或者遇到一个非数字符号为止
如果第一个参数传入的Number类型:
数字如果是0开头,则将其当作八进制来解析(如果是一个八进制数);如果以0x开头,则将其当作十六进制来解析
如果第一个参数是 null 或者是 undefined,或者是一个对象类型:
返回 NaN
如果第一个参数是数组:
1. 去数组的第一个元素,按照上面的规则进行解析
如果第一个参数是Symbol类型:
1. 抛出错误
如果指定radix参数,以radix为基数进行解析:
parseInt('0111'); //111
parseInt(0111); //八进制数 73
parseInt('');//NaN
parseInt('0X11'); //17
parseInt('1a') //1
parseInt('a1'); //NaN
parseInt(['10aa','aaa']);//10
parseInt([]);//NaN; parseInt(undefined);
parseFloat
规则和parseInt基本相同,接受一个Number类型或字符串,如果是字符串中,那么只有第一个小数点是有效的。
toString()
规则如下:
如果是Number类型,输出数字字符串
如果是 null 或者是 undefined,抛错
如果是数组,那么将数组展开输出。空数组,返回''
如果是对象,返回 [object Object]
如果是Date, 返回日期的文字表示法
如果是函数,输出对应的字符串(如下demo)
如果是Symbol,输出Symbol字符串
let arry = [];
let obj = {a:1};
let sym = Symbol(100);
let date = new Date();
let fn = function() {console.log('稳住,我们能赢!')}
let str = 'hello world';
console.log([].toString()); // ''
console.log([1, 2, 3, undefined, 5, 6].toString());//1,2,3,,5,6
console.log(arry.toString()); // 1,2,3
console.log(obj.toString()); // [object Object]
console.log(date.toString()); // Sun Apr 21 2019 16:11:39 GMT+0800 (CST)
console.log(fn.toString());// function () {console.log('稳住,我们能赢!')}
console.log(str.toString());// 'hello world'
console.log(sym.toString());// Symbol(100)
console.log(undefined.toString());// 抛错
console.log(null.toString());// 抛错
String()
String() 的转换规则与 toString() 基本一致,最大的一点不同在于 null 和 undefined,使用 String 进行转换,null 和 undefined对应的是字符串 'null' 和 'undefined'
Boolean
除了 undefined、 null、 false、 ''、 0(包括 +0,-0)、 NaN 转换出来是false,其它都是true.
隐式类型转换
&& 、|| 、 ! 、 if/while 的条件判断
需要将数据转换成 Boolean 类型,转换规则同 Boolean 强制类型转换
运算符: + - * /
+ 号操作符,不仅可以用作数字相加,还可以用作字符串拼接。
仅当 + 号两边都是数字时,进行的是加法运算。如果两边都是字符串,直接拼接,无需进行隐式类型转换。
除了上面的情况外,如果操作数是对象、数值或者布尔值,则调用toString()方法取得字符串值(toString转换规则)。对于 undefined 和 null,分别调用String()显式转换为字符串,然后再进行拼接。
console.log({}+10); //[object Object]10
console.log([1, 2, 3, undefined, 5, 6] + 10);//1,2,3,,5,610
-、*、/ 操作符针对的是运算,如果操作值之一不是数值,则被隐式调用Number()函数进行转换。如果其中有一个转换除了为NaN,结果为NaN.
关系操作符: > , < ,<= ,>=
1. 如果两个操作值都是数值,则进行数值比较
2. 如果两个操作值都是字符串,则比较字符串对应的字符编码值
3. 如果有一方是Symbol类型,抛出错误
4. 除了上述情况之外,都进行Number()进行类型转换,然后再进行比较。
注:NaN是非常特殊的值,它不和任何类型的值相等,包括它自己,同时它与任何类型的值比较大小时都返回false。
console.log(10 > {});//返回false.
/**
*{}.valueOf ---> {}
*{}.toString() ---> '[object Object]' ---> NaN
*NaN 和 任何类型比大小,都返回 false
*/
相等操作符:==
如果类型相同,无需进行类型转换。
如果其中一个操作值是 null 或者是 undefined,那么另一个操作符必须为 null 或者 undefined 时,才返回 true,否则都返回 false.
如果其中一个是 Symbol 类型,那么返回 false.
两个操作值是否为 string 和 number,就会将字符串转换为 number
如果一个操作值是 boolean,那么转换成 number
如果一个操作值为 object 且另一方为 string、number 或者 symbol,是的话就会把 object 转为原始类型再进行判断(调用object的valueOf/toString方法进行转换)
对象如何转换成原始数据类型
如果部署了 [Symbol.toPrimitive] 接口,那么调用此接口,若返回的不是基础数据类型,抛出错误。
如果没有部署 [Symbol.toPrimitive] 接口,那么先返回 valueOf() 的值,若返回的不是基础类型的值,再返回 toString() 的值,若返回的不是基础类型的值, 则抛出异常。
//先调用 valueOf, 后调用 toString
let obj = {
[Symbol.toPrimitive]() {
return 200;
},
valueOf() {
return 300;
},
toString() {
return 'Hello';
}
}
//如果 valueOf 返回的不是基本数据类型,则会调用 toString,
//如果 toString 返回的也不是基本数据类型,会抛出错误
console.log(obj + 200); //400
8.简述下对 webWorker 的理解?
HTML5则提出了 Web Worker 标准,表示js允许多线程,但是子线程完全受主线程控制并且不能操作dom,只有主线程可以操作dom,所以js本质上依然是单线程语言。
web worker就是在js单线程执行的基础上开启一个子线程,进行程序处理,而不影响主线程的执行,当子线程执行完之后再回到主线程上,在这个过程中不影响主线程的执行。子线程与主线程之间提供了数据交互的接口postMessage和onmessage,来进行数据发送和接收。
var worker = new Worker('./worker.js'); //创建一个子线程
worker.postMessage('Hello');
worker.onmessage = function (e) {
console.log(e.data); //Hi
worker.terminate(); //结束线程
};
//worker.js
onmessage = function (e) {
console.log(e.data); //Hello
postMessage("Hi"); //向主进程发送消息
};
仅是最简示例代码,项目中通常是将一些耗时较长的代码,放在子线程中运行。
9.ES6模块和CommonJS模块的差异?
1. ES6模块在编译时,就能确定模块的依赖关系,以及输入和输出的变量。
2. CommonJS 模块,运行时加载。
3. ES6 模块自动采用严格模式,无论模块头部是否写了 "use strict";
4. require 可以做动态加载,import 语句做不到,import 语句必须位于顶层作用域中。
5. ES6 模块中顶层的 this 指向 undefined,commonJS 模块的顶层 this 指向当前模块。
6. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。如:
//name.js
var name = 'William';
setTimeout(() => name = 'Yvette', 200);
module.exports = {
name
};
//index.js
const name = require('./name');
console.log(name); //William
setTimeout(() => console.log(name), 300); //William
对比 ES6 模块看一下:
ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import ,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
//name.js
var name = 'William';
setTimeout(() => name = 'Yvette', 200);
export { name };
//index.js
import { name } from './name';
console.log(name); //William
setTimeout(() => console.log(name), 300); //Yvette
10.浏览器事件代理机制的原理是什么?
在说浏览器事件代理机制原理之前,我们首先了解一下事件流的概念,早期浏览器,IE采用的是事件捕获事件流,而Netscape采用的则是事件捕获。"DOM2级事件"把事件流分为三个阶段,捕获阶段、目标阶段、冒泡阶段。现代浏览器也都遵循此规范。
那么事件代理是什么呢?
事件代理又称为事件委托,在祖先级DOM元素绑定一个事件,当触发子孙级DOM元素的事件时,利用事件冒泡的原理来触发绑定在祖先级DOM的事件。因为事件会从目标元素一层层冒泡至document对象。
为什么要事件代理?
1. 添加到页面上的事件数量会影响页面的运行性能,如果添加的事件过多,会导致网页的性能下降。采用事件代理的方式,可以大大减少注册事件的个数。
2. 事件代理的当时,某个子孙元素是动态增加的,不需要再次对其进行事件绑定。
3. 不用担心某个注册了事件的DOM元素被移除后,可能无法回收其事件处理程序,我们只要把事件处理程序委托给更高层级的元素,就可以避免此问题。
如将页面中的所有click事件都代理到document上:
addEventListener 接受3个参数,分别是要处理的事件名、处理事件程序的函数和一个布尔值。布尔值默认为false。表示冒泡阶段调用事件处理程序,若设置为true,表示在捕获阶段调用事件处理程序。
document.addEventListener('click', function (e) {
console.log(e.target);
/**
* 捕获阶段调用调用事件处理程序,eventPhase是 1;
* 处于目标,eventPhase是2
* 冒泡阶段调用事件处理程序,eventPhase是 1;
*/
console.log(e.eventPhase);
});
11.js如何自定义事件?
自定义 DOM 事件(不考虑IE9之前版本)
自定义事件有三种方法,一种是使用 new Event(), 另一种是 createEvent('CustomEvent') , 另一种是 new customEvent()
1.使用 new Event()获取不到 event.detail
let btn = document.querySelector('#btn');
let ev = new Event('alert', {
bubbles: true, //事件是否冒泡;默认值false
cancelable: true, //事件能否被取消;默认值false
composed: false
});
btn.addEventListener('alert', function (event) {
console.log(event.bubbles); //true
console.log(event.cancelable); //true
console.log(event.detail); //undefined
}, false);
btn.dispatchEvent(ev);
2.使用 createEvent('CustomEvent') (DOM3)
要创建自定义事件,可以调用 createEvent('CustomEvent'),返回的对象有 initCustomEvent 方法,接受以下四个参数:
1. type: 字符串,表示触发的事件类型,如此处的'alert'
2. bubbles: 布尔值:表示事件是否冒泡
3. cancelable: 布尔值,表示事件是否可以取消
4. detail: 任意值,保存在 event 对象的 detail 属性中
let btn = document.querySelector('#btn');
let ev = btn.createEvent('CustomEvent');
ev.initCustomEvent('alert', true, true, 'button');
btn.addEventListener('alert', function (event) {
console.log(event.bubbles); //true
console.log(event.cancelable);//true
console.log(event.detail); //button
}, false);
btn.dispatchEvent(ev);
3. 使用 new customEvent() (DOM4)
使用起来比 createEvent('CustomEvent') 更加方便。
var btn = document.querySelector('#btn');
/*
* 第一个参数是事件类型
* 第二个参数是一个对象
*/
var ev = new CustomEvent('alert', {
bubbles: 'true',
cancelable: 'true',
detail: 'button'
});
btn.addEventListener('alert', function (event) {
console.log(event.bubbles); //true
console.log(event.cancelable);//true
console.log(event.detail); //button
}, false);
btn.dispatchEvent(ev);
自定义非 DOM 事件(观察者模式)
EventTarget类型有一个单独的属性handlers,用于存储事件处理程序(观察者)。
addHandler() 用于注册给定类型事件的事件处理程序;
fire() 用于触发一个事件;
removeHandler() 用于注销某个事件类型的事件处理程序。
function EventTarget(){
this.handlers = {};
}
EventTarget.prototype = {
constructor:EventTarget,
addHandler:function(type,handler){
if(typeof this.handlers[type] === "undefined"){
this.handlers[type] = [];
}
this.handlers[type].push(handler);
},
fire:function(event){
if(!event.target){
event.target = this;
}
if(this.handlers[event.type] instanceof Array){
const handlers = this.handlers[event.type];
handlers.forEach((handler)=>{
handler(event);
});
}
},
removeHandler:function(type,handler){
if(this.handlers[type] instanceof Array){
const handlers = this.handlers[type];
for(var i = 0,len = handlers.length; i < len; i++){
if(handlers[i] === handler){
break;
}
}
handlers.splice(i,1);
}
}
}
//使用
function handleMessage(event){
console.log(event.message);
}
//创建一个新对象
var target = new EventTarget();
//添加一个事件处理程序
target.addHandler("message", handleMessage);
//触发事件
target.fire({type:"message", message:"Hi"}); //Hi
//删除事件处理程序
target.removeHandler("message",handleMessage);
//再次触发事件,没有事件处理程序
target.fire({type:"message",message: "Hi"});
12.跨域的方法有哪些?原理是什么?
在说跨域方法之前,我们先了解下什么叫跨域,浏览器有同源策略,只有当“协议”、“域名”、“端口号”都相同时,才能称之为是同源,其中有一个不同,即是跨域。
那么同源策略的作用是什么呢?同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。
那么我们又为什么需要跨域呢?一是前端和服务器分开部署,接口请求需要跨域,二是我们可能会加载其它网站的页面作为iframe内嵌。
跨域的方法有哪些?
常用的跨域方法
1. jsonp
尽管浏览器有同源策略,但是