console.log("AAAA");
setTimeout(() => console.log("BBBB"), 1000);
const start = new Date();
while (new Date() - start < 3000) { }
console.log("CCCC");
setTimeout(() => console.log("DDDD"), 0);
new Promise((resolve, reject) => {
console.log("EEEE");
foo.bar(100);
})
.then(() => console.log("FFFF"))
.then(() => console.log("GGGG"))
.catch(() => console.log("HHHH"));
console.log("IIII");
答案解析:这道题考察重点是 js异步执行 宏任务 微任务。
- 一开始代码执行,输出
AAAA
. 1 - 第二行代码开启一个计时器t1(一个称呼),这是一个异步任务且是宏任务,需要等到1秒后提交。
- 第四行是个while语句,需要等待3秒后才能执行下面的代码,这里有个问题,就是3秒后上一个计时器t1的提交时间已经过了,但是线程上的任务还没有执行结束,所以暂时不能打印结果,所以它排在宏任务的最前面了。
- 第五行又输出
CCCC
- 第六行又开启一个计时器t2(称呼),它提交的时间是0秒(其实每个浏览器器有默认最小时间的,暂时忽略),但是之前的t1任务还没有执行,还在等待,所以t2就排在t1的后面。(t2排在t1后面的原因是while造成的)都还需要等待,因为线程上的任务还没执行完毕。
- 第七行
new Promise
将执行promise函数,它参数是一个回调函数,这个回调函数内的代码是同步的,它的异步核心在于resolve和reject,同时这个异步任务在任务队列中属于微任务,是优先于宏任务执行的,(不管宏任务有多急,反正我是VIP)。所以先直接打印输出同步代码EEEE
。第九行中的代码是个不存在的对象,这个错误要抛给reject这个状态,也就是catch去处理,但是它是异步的且是微任务,只有等到线程上的任务执行完毕,立马执行它,不管宏任务(计时器,ajax等)等待多久了。 - 第十四行,这是线程上的最后一个任务,打印输出
IIII
- 我们先找出线程上的同步代码,将结果依次排列出来:AAAA CCCC EEEE IIII
- 然后我们再找出所有异步任务中的微任务 把结果打印出来 HHHH
- 最后我们再找出异步中的所有宏任务,这里t1排在前面t2排在后面(这个原因是while造成的),输出结果顺序是 BBBB DDDD
- 所以综上 结果是 AAAA CCCC EEEE IIII HHHH BBBB DDDD
下一题
async function async1() {
console.log("AAAA");
async2();
console.log("BBBB");
}
async function async2() {
console.log("CCCC");
}
console.log("DDDD");
setTimeout(function () {
console.log("FFFF");
}, 0);
async1();
new Promise(function (resolve) {
console.log("GGGG");
resolve();
}).then(function () {
console.log("HHHH");
});
console.log("IIII");
答案解析:这道题考察重点是 js异步执行 宏任务 微任务.
- 这道题的坑就在于 async中如果没有await,那么它就是一个纯同步函数。
- 这道题的起始代码在第9行,输出
DDDD
- 第10行计时器开启一个异步任务t1(一个称呼),这个任务且为宏任务。
- 第13行函数
async1
执行,这个函数内没有await 所以它其实就是一个纯同步函数,打印输出AAAA
, - 在
async1
中执行async2
函数,因为async2
的内部也没有await,所以它也是个纯同步函数,打印输出CCCC
- 紧接着打印输出
BBBB
。 - 第14行new Promise执行里面的代码也是同步的,所以打印输出
GGGG
,resolve()调用的时候开启一个异步任务t2(一个称呼),且这个任务t2是微任务,它的执行交给then()中的第一个回调函数执行,且优先级高于宏任务(t1)执行。 - 第20行打印输出
IIII
,此时线程上的同步任务全部执行结束。 - 在执行任务队列中的异步任务时,微任务优先于宏任务执行,所以先执行微任务 t2 打印输出
HHHH
,然后执行宏任务 t1 打印输出FFFF
- 所以综上 结果输出是 DDDD AAAA CCCC BBBB GGGG IIII HHHH FFFF
下一题
async function t2() {
let a = await new Promise((resolve) => {});
console.log(a);//
}
t2()
答案解析:
await
后面如果跟一个promise对象,await将等待这个promise对象的resolve状态的值value,且将这个值返回给前面的变量,此时的promise对象的状态是一个pending状态,没有resolve状态值,所以什么也打印不了。
下一题
async function t3() {
let a = await new Promise((resolve) => {
resolve();
});
console.log(a);//undefined
}
t3()
答案解析:
await
后面如果跟一个promise对象,await将等待这个promise对象的resolve状态的值value,且将这个值返回给前面的变量,此时的promise对象的状态是一个resolve状态,但是它的状态值是undefined,所以打印出undefined。
下一题
async function t5() {
let a = await new Promise((resolve) => {
resolve("hello");
}).then(() => {
return "lala";
});
console.log(a);//lala
}
t5()
答案解析:
await
后面如果跟一个promise对象,await将等待这个promise对象的resolve状态的值,且将这个值返回给前面的变量,此时的promise对象的状态是一个resolve状态,它的状态值是hello,紧接着后面又执行了一个then方法,then方法又会返回一个全新的promise对象,且这个then方法中的返回值会作为这个全新的promise中resolve的值,所以最终的结果是lala。
下一题
async function t6() {
let a = await fn().then((res) => {
return res
})
console.log(a);//undefined
}
async function fn(){
await new Promise((resolve)=>{
resolve("lagou")
})
}
t6()
答案解析:
async
函数执行返回一个promise
对象,且async
函数内部的返回值会当作这个promise对象resolve状态的值
下一题
async function fn() {
return "la";
}
var p = fn();
console.log(p);
答案解析:
Promise {<fulfilled>: "la"}
__proto__: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: "la"
下一题
function Foo() {
getName = function () { // 变量提升至window下
console.log(1);
};
console.log(this)
debugger
return this;
}
Foo.getName = function () {
console.log(2);
};
Foo.prototype.getName = function () {
console.log(3);
};
var getName = function () {
console.log(4);
};
function getName() {
console.log(5);
}
Foo.getName(); // 静态方法只能通过直接访问 // 2
Foo().getName(); // Foo()返回实际上是window, 原型上的方法和属性只能通过实例来访问, // 1
getName(); // 1 在执行过Foo().getName()的基础上,所以getName=function(){console.log(1)},所以,打印1,[如果getName()放在Foo().getName()上执行打印结果为4]
new Foo.getName(); // 2 构造器私有属性的getName(),所以,打印2
new Foo().getName(); // 3 new 返回实例调用的是原型上的getName(),打印3
new new Foo().getName(); // 3 首先new Foo()得到一个空对象{}、 第二步向空对象中添加一个属性getName,值为一个函数、 第三步new {}.getName() 、等价于 var bar = new (new Foo().getName)(); console.log(bar)
// 先new Foo得到的实例对象上的getName方法,再将这个原型上getName方法当做构造函数继续new ,所以执行原型上的方法,打印3
下一题
promise.then((res) => {
console.log('then:', res);
}).catch((err) => {
console.log('catch:', err);
})
//第二种
promise.then((res) => {
console.log('then:', res);
}, (err) => {
console.log('catch:', err);
})
答案解析:
- 第一种 catch 方法可以捕获到 catch 之前整条 promise 链路上所有抛出的异常
- 第二种 then 方法的第二个参数捕获的异常依赖于上一个 Promise 对象的执行结果。
- promise.then(successCb, faildCd) 接收两个函数作为参数,来处理上一个promise 对象的结果。then f 方法返回的是 promise 对象。第一种链式写法,使用catch,相当于给前面一个then方法返回的promise 注册回调,可以捕获到前面then没有被处理的异常。第二种是回调函数写法,仅为为上一个promise 注册异常回调。
- 如果是promise内部报错 reject 抛出错误后,then 的第二个参数就能捕获得到,如果then的第二个参数不存在,则catch方法会捕获到。
- 如果是then的第一个参数函数 resolve 中抛出了异常,即成功回调函数出现异常后,then的第二个参数reject 捕获捕获不到,catch方法可以捕获到。
下一题
var length = 10;
function fn() {
console.log(this.length);
}
var obj = {
length: 5,
method: function(fn) {
fn();
console.log('arguments', arguments)
arguments[0](); // arguments[0] this指向arguments
}
};
obj.method(fn, 1); // 10, 5
答案解析:
-
首先,我们在全局定义了一个变量length、一个对象obj和一个函数fn,length赋值为10。接下来是fn函数,输出this.length。对象obj中,obj.length是5,obj.method是一个函数。method函数里面的形参也是一个函数,这个函数里面调用了fn函数,arguments是一个伪数组,代表method函数实际接收到的参数列表,所以arguments[0] ()就代表了调用arguments里的第一项。obj.method(fn, 1)代表的就是调用obj当中的method函数,并且传递了两个参数,fn和1。
-
分析完了代码的含义,我们来看输出结果。method函数当中调用的fn函数是全局当中的函数,所以this指向的是window,this.length就是10。上面说了,arguments[0] ()代表的是调用arguments里面的第一项,也就是传参进来的fn,所以这个this指向的是arguments,method函数接收的参数是两个,所以arguments.length就是2。最后的输出结果就是 10 2
下一题
function a(xx){
this.x = xx;
return this; // window.x = 5 -> window.x = 6
};
var x = a(5); // x = window
var y = a(6); // y = window
console.log(x.x); // undefined
console.log(y.x); // 6
答案解析:
-
首先,我们在全局定义了一个变量x、一个变量y和一个函数a,函数a当中的this.x等于接收到的参数,返回this,这里要注意,返回的不是this.x,而是this。接下来我们给x赋值,值为a(5),又给y进行赋值,值为a(6)。最后,我们输出x.x,y.x。
-
分析完代码的含义,我们来看输出结果。a函数传了一个参数5,那么this.x就被赋值为了5,函数a的this指向的是window,也就是window.x = 5。上面我们说过,这个函数返回的是this,也就是this指向的window,x = a(5)就相当于window.x = window,此时的x被赋值为了window。下面又执行了y = a(6),也就是说,x的值再次发生了改变,边为了6,y则被赋值为了window。console.log(x.x)就相当于console.log(6.x),输出的自然是undefined。console.log(y.x),输出的相当于是console.log(window.x),得到的值自然是6。最后输出的结果为 undefined 6
下一题
new Promise((resolve, reject) => { // 微任务 1 - 1
console.log("A");
setTimeout(() => { // 宏任务 1 -1
console.log("B");
}, 0);
console.log("C");
resolve();
console.log("D");
})
.then(() => { // 微任务 2 - 2
console.log("E");
new Promise((resolve, reject) => { // 微任务 2 - 1
console.log("F");
resolve();
console.log("G");
})
.then(() => { // 微任务 3 - 1
setTimeout(() => {
console.log("H");
}, 0);
console.log("I");
})
.then(() => { // 微任务 3 - 3
console.log("J");
});
})
.then(() => { // 微任务 3 - 2
console.log("K");
});
setTimeout(() => { // 宏任务 1 - 2
console.log("L");
}, 0);
new Promise((resolve, reject) => { // 微任务 1 - 2
console.log("M");
resolve();
}).then(() => { // // 微任务 2-3
setTimeout(() => { // 宏任务 1 - 3
new Promise((resolve, reject) => {
console.log("N");
resolve();
})
.then(() => {
setTimeout(() => {
console.log("O");
}, 0);
})
.then(() => {
console.log("P");
});
}, 0);
});
console.log("Q");
// 同步代码: A -> C -> D -> M -> Q
// 第一轮微任务: E ->F ->G
// 第二微轮任务: I -> K -> J
// 后续宏任务: B -> L -> N -> P -> H -> O
答案解析:
-
首先,我们要知道微任务会先于宏任务执行。知道了这一点,我们来看下面的代码。
-
还是先看最外层的结构,Promise类-定时器-Promise类-console.log。先执行同步代码,打印出第一个Promise类里面的A、C、D,第一个Promise里面的定时器进去宏任务列表,排在第一位。这个Promise产生的微任务进入第一轮微任务列表,排在第一位。最外层定时器进入宏任务列表,排在第二位。打印第二个Promise类里面的M,第二个Promise产生的微任务进入第一轮微任务列表,排在第二位。最后执行console.log("Q"),打印出Q。
-
微任务会先于宏任务执行,所以先执行第一个Promise的第一个then,打印E、F、G。这个then里面又生成了一个新的Promise类,同理,新的Promise产生的微任务进入第二轮微任务列表,排在第一位。then会返回一个Promise类,也就是说这个then也会产生一个微任务,进入到第二轮微任务列表,排在第二位。接下来执行第二个Promise的第一个then,这个then里面是一个定时器,进入到宏任务列表,排在第三位。本轮微任务结束。
-
第二轮微任务,先执行第一个Promise的第一个then里面的Promise类的第一个then,then里面的定时器进入宏任务列表,排在第四位,执行console.log("I"),打印I。这个then返回的Promise类生成的微任务进入到第三轮的微任务列表中,排在第一位。继续执行第一个Promise的第二个then,打印K。本轮微任务结束。
-
第三轮微任务,执行第一个Promise的第一个then里面的Promise类的第二个then,打印J。执行到这里,微任务已经全部执行完毕,开始执行宏任务。
-
执行第一个Promise里面的定时器,打印B。执行最外层的定时器,打印L。执行第二个Promise的第一个then里面的定时器,生成一个新的Promise类,打印这个Promise类里面的N,生成一个微任务,加入到微任务列表中。执行到这里,宏任务列表里面还有一个任务未执行,由于微任务会先于宏任务执行,所以宏任务会暂停执行,先执行微任务。
-
执行的是第二个Promise的then里面的Promise的第一个then,这个then里面是定时器,加入到宏任务列表里面,排在第二位。这个then返回的Promise生成一个微任务,进入下一轮的微任务列表。
-
执行第二个Promise的then里面的Promise的第二个then,打印P。执行到这里,微任务已经全部执行完毕,开始执行宏任务。
-
执行第一个Promise的第一个then里面的Promise类的第一个then里面的定时器,打印H。
-
最后执行第二个Promise的then里面的Promise类的第一个then里面的定时器,打印O。
下一题
在你开发的过程中,什么情况下会遇到跨域问题,你是怎么解决的?
答案解析:
-
- API跨域可以通过服务器上nginx反向代理
-
- 本地webpack dev server可以设置 proxy,
-
- new Image, 设src 的时候,图片需要设置Cors
-
- cors需要后台配合设置HTTP响应头,如果请求不是简单请求(1. method:get,post,2. content-type:三种表单自带的content-type,3. 没有自定义的HTTP header),浏览器会先发送option预检请求,后端需要响应option请求,然后浏览器才会发送正式请求,cors通过白名单的形式允许指定的域发送请求
-
- jsonp是浏览器会放过 img script标签引入资源的方式。所以可以通过后端返回一段执行js函数的脚本,将数据作为参数传入。然后在前端执行这段脚本。双方约定一个函数的名称。
- 4.联调的时候会需要跨域,线上前端站点域和后台接口不一致也需要跨域,开发时跨域可以通过代理服务器来转发请求,因为跨域本身是浏览器对请求的限制,常见的跨域处理还有JSONP和cors,jsonp是利用脚本资源请求本身就可以跨域的特性,
-
- 同源策略只是浏览器客户端的防护机制,当发现非同源HTTP请求时会拦截响应,但服务器依然处理了这个请求。
-
- 服务器端不拦截,所以在同源服务器下做代理,可以实现跨域。
下一题
JS 异步解决方案的发展历程以及优缺点
-
- 回调函数(callback)
setTimeout(() => { // callback 函数体 }, 1000)
- 缺点:回调地狱,不能用 try catch 捕获错误,不能 return
- 回调地狱的根本问题在于:
- 缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符;
- 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转);
- 嵌套函数过多的多话,很难处理错误。
ajax('XXX1', () => { // callback 函数体 ajax('XXX2', () => { // callback 函数体 ajax('XXX3', () => { // callback 函数体 }) }) })
- 优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行)。
-
- Promise
- Promise 就是为了解决 callback 的问题而产生的。
- Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装。
- 优点:解决了回调地狱的问题。
ajax('XXX1') .then(res => { // 操作逻辑 return ajax('XXX2') }).then(res => { // 操作逻辑 return ajax('XXX3') }).then(res => { // 操作逻辑 })
- 缺点:无法取消 Promise ,错误需要通过回调函数来捕获。
-
- Generator
- 特点:可以控制函数的执行,可以配合 co 函数库使用。
function *fetch() { yield ajax('XXX1', () => {}) yield ajax('XXX2', () => {}) yield ajax('XXX3', () => {}) } let it = fetch() let result1 = it.next() let result2 = it.next() let result3 = it.next()
-
4.Async/await
- async、await 是异步的终极解决方案
- 优点是:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题;
- 缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。
async function test() { // 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式 // 如果有依赖性的话,其实就是解决回调地狱的例子了 await fetch('XXX1') await fetch('XXX2') await fetch('XXX3') }
下面来看一个使用 await 的例子:
let a = 0 let b = async () => { a = a + await 10 console.log('2', a) // -> '2' 10 } b() a++ console.log('1', a) // -> '1' 1
- 对于以上代码你可能会有疑惑,让我来解释下原因:
- 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为 await 内部实现了 generator ,generator 会保留堆栈中东西,所以这时候 a = 0 被保存了下来;
- 因为 await 是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码;
- 同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10。
- 上述解释中提到了 await 内部实现了 generator,其实 await 就是 generator 加上 Promise的语法糖,且内部实现了自动执行 generator。如果你熟悉 co 的话,其实自己就可以实现这样的语法糖。方案。
下一题
什么是闭包, 如何使用它, 为会要使用它?
答案解析:
- 闭包就是能够读取其他函数内部变量的函数。由于在Javascript语言中,
- 只有函数内部的子函数才能读取局部变量,
- 因此可以把闭包简单理解成“定义在一个函数内部的函数”。
- 所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
- 使用闭包的注意点:
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值
下一题
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。注意你不能在买入股票前卖出股票。
实例
- 输入: [7,1,5,3,6,4]
- 输出: 5
- 解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
const arrs = [7,1,5,3,6,4]
function getMaxProfit(prices) {
// 最小的价格
let minPrice = Number.MAX_SAFE_INTEGER
// 最大的利益
let maxProfile = 0;
for (let i = 0; i < prices.length; i++) {
if (prices[i] <= minPrice) {
minPrice = Math.min(minPrice, prices[i])//更新最小价格
} else {
// 更新最大收益
maxProfile = Math.max(prices[i] - minPrice, maxProfile)
}
}
return maxProfile;
}
console.log(getMaxProfit(arrs))
下一题
将一个 html 字符串变成树的形式
<div id="main" data-x="hello">Hello<span id="sub" /></div>
- 这样的一串字符串变成如下的一棵树,考虑尽可能多的形式,比如自闭合标签等。
{
tag: "div",
selfClose: false,
attributes: {
"id": "main",
"data-x": "hello"
},
text: "Hello",
children: [
{
tag: "span",
selfClose: true,
attributes: {
"id": "sub"
}
}
]
}
答案解析
- 方法1:通过childNodes获取节点,递归children属性
const html = `<div id="main" data-x="hello">Hello<span id="sub" /></div>`
const div = document.createElement('div')
div.innerHTML = html;
console.log(div.childNodes);
//通过遍历div.childNodes构造树
- 方法二:通过html-parse-stringify2包解析
var HTML = require('html-parse-stringify2')
var html = `<div id="main" data-x="hello">Hello<span id="sub" /></div>`;
var ast = HTML.parse(html);
console.log(ast);
下一题
JavaScript不支持函数重载,但TypeScript是否支持函数重载?
是的,TypeScript支持函数重载。但是它的实现很奇怪,当我们在TypeScript中执行函数重载时,我们只能实现一个带有多个签名的函数。
//带有字符串类型参数的函数
function add(a:string, b:string): string;
//带有数字类型参数的函数
function add(a:number, b:number): number;
//函数定义
function add(a: any, b:any): any {
return a + b;
}
在上面的例子中,前两行是函数重载声明。它有两次重载,第一个签名的参数类型为string,而第二个签名的参数类型为number。第三个函数包含实际实现并具有any类型的参数。任何数据类型都可以接受任何类型的数据。然后,实现检查所提供参数的类型,并根据供应商参数类型执行不同的代码段
下一题
在前端工程化涌现出众多工具, 试说明webpack与grunt、gulp的不同?
-
三者都是前端构建工具,grunt和gulp在早期比较流行,现在webpack相对来说比较主流,不过一些轻量化的任务还是会用gulp来处理,比如单独打包CSS文件等。
-
grunt和gulp是基于任务和流(Task、Stream)的。类似jQuery,找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个web的构建流程。
-
webpack是基于入口的。webpack会自动地递归解析入口所需要加载的所有资源文件,然后用不同的Loader来处理不同的文件,用Plugin来扩展webpack功能。
-
从构建思路来说
- gulp和grunt需要开发者将整个前端构建过程拆分成多个
Task
,并合理控制所有Task
的调用关系 webpack需要开发者找到入口,并需要清楚对于不同的资源应该使用什么Loader做何种解析和加工复制代码 - gulp更像后端开发者的思路,需要对于整个流程了如指掌 webpack更倾向于前端开发者的思路
- gulp和grunt需要开发者将整个前端构建过程拆分成多个
答案解析:
下一题
npm打包时需要注意哪些?如何利用webpack来更好的构建?
Npm
是目前最大的 JavaScript 模块仓库,里面有来自全世界开发者上传的可复用模块。你可能只是JS模块的使用者,但是有些情况你也会去选择上传自己开发的模块。 关于NPM模块上传的方法可以去官网上进行学习,这里只讲解如何利用webpack来构建- NPM模块需要注意以下问题:
- 1.要支持CommonJS模块化规范,所以要求打包后的最后结果也遵守该规则。
-
- Npm模块使用者的环境是不确定的,很有可能并不支持ES6,所以打包的最后结果应该是采用ES5编写的。并且如果ES5是经过转换的,请最好连同SourceMap一同上传。
-
- Npm包大小应该是尽量小(有些仓库会限制包大小)
-
- 发布的模块不能将依赖的模块也一同打包,应该让用户选择性的去自行安装。这样可以避免模块应用者再次打包时出现底层模块被重复打包的情况。
-
- UI组件类的模块应该将依赖的其它资源文件,例如
.css
文件也需要包含在发布的模块里。
- UI组件类的模块应该将依赖的其它资源文件,例如
- 基于以上需要注意的问题,我们可以对于webpack配置做以下扩展和优化:
- 1.CommonJS模块化规范的解决方案: 设置output.libraryTarget='commonjs2'使输出的代码符合CommonJS2 模块化规范,以供给其它模块导入使用
- 2.输出ES5代码的解决方案:使用babel-loader把 ES6 代码转换成 2.ES5 的代码。再通过开启devtool: 'source-map'输出SourceMap以发布调试。
- 3.Npm包大小尽量小的解决方案:Babel 在把 ES6 代码转换成 ES5 代码时会注入一些辅助函数,最终导致每个输出的文件中都包含这段辅助函数的代码,造成了代码的冗余。解决方法是修改.babelrc文件,为其加入transform-runtime插件
- 4.不能将依赖模块打包到NPM模块中的解决方案:使用externals配置项来告诉webpack哪些模块不需要打包。
- 5.对于依赖的资源文件打包的解决方案:通过css-loader和extract-text-webpack-plugin来实现,配置如下:
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
module: {
rules: [
{
// 增加对 CSS 文件的支持
test: /\.css/,
// 提取出 Chunk 中的 CSS 代码到单独的文件中
use: ExtractTextPlugin.extract({
use: ['css-loader']
}),
},
]
},
plugins: [
new ExtractTextPlugin({
// 输出的 CSS 文件名称
filename: 'index.css',
}),
],
};
下一题
说一下在webpack中的loader和plugin的不同
- 不同的作用
- Loader:直译为"加载器"。Webpack将一切文件视为模块,但是webpack原生是只能解析js文件,如果想将其他文件也打包的话,就会用到
loader
。 所以Loader的作用是让webpack拥有了加载和解析非JavaScript文件的能力。 - Plugin: 直译为"插件"。Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
- Loader:直译为"加载器"。Webpack将一切文件视为模块,但是webpack原生是只能解析js文件,如果想将其他文件也打包的话,就会用到
- 不同的用法
- Loader: 在
module.rules
中配置,也就是说他作为模块的解析规则而存在。 类型为数组,每一项都是一个Object
,里面描述了对于什么类型的文件(test
),使用什么加载(loader
)和使用的参数(options
) - Plugin:在
plugins
中单独配置。 类型为数组,每一项是一个plugin
的实例,参数都通过构造函数传入。
- Loader: 在
下一题
webpack的构建流程是什么?从读取配置到输出文件这个过程尽量说全
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
- 1、初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
- 2.开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
-
- 确定入口:根据配置中的 entry 找出所有的入口文件;
-
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
-
- 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
-
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
-
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。 在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果
答案解析:
下一题
Vue中的双向数据绑定是如何实现的
- Vue的双向数据绑定是通过数据劫持结合发布者订阅者模式来实现的, 要实现这种双向数据绑定,必要的条件有:
- 实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
- 实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
- 实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数、从而更新视图
- 4、MVVM入口函数,整合以上三者
个人理解:在new Vue的时候,在Observer中通过Object.defineProperty()达到数据劫持,代理所有数据的getter和setter属性,在每次触发setter的时候,都会通过Dep来通知Watcher,Watcher作为Observer数据监听器与Compile模板解析器之间的桥梁,当Observer监听到数据发生改变的时候,通过Updater来通知Compile更新视图** 而Compile通过Watcher订阅对应数据,绑定更新函数,通过Dep来添加订阅者,达到双向绑定
答案解析:
下一题
写 React/Vue 项目时为什么要在组件中写 key,其作用是什么?
- key 的作用是为了在 diff 算法执行时更快的找到对应的节点,提高 diff 速度。
- vue 和 react 都是采用 diff 算法来对比新旧虚拟节点,从而更新节点。在 vue 的 diff 函数中。可以先了解一下 diff 算法。
- 在交叉对比的时候,当新节点跟旧节点头尾交叉对比没有结果的时候,会根据新节点的 key 去对比旧节点数组中的 key,从而找到相应旧节点(这里对应的是一个 key => index 的 map 映射)。如果没找到就认为是一个新增节点。而如果没有 key,那么就会采用一种遍历查找的方式去找到对应的旧节点。一种一个 map 映射,另一种是遍历查找。相比而言。map 映射的速度更快。
答案解析:
下一题
webpack 中 tree shaking 的用途和原理是什么?
- tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如 import 和 export。这个术语和概念实际上是兴起于 ES2015 模块打包工具 rollup。
- 新的 webpack 4 正式版本,扩展了这个检测能力,通过 package.json 的 "sideEffects" 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 "pure(纯的 ES2015 模块)",由此可以安全地删除文件中未使用的部分。
- tree shaking的概念在1990年就提出了,但是直到ES6的ES6-style模块出现以后才真正被利用起来。这是因为tree shaking只能在静态modules下工作。ECMAScript 6 模块加载是静态的,因此整个依赖树可以被静态地推导出解析语法树。所以在ES6中使用tree shaking是非常容易的。而且,tree shaking不仅支持import/export级别,还支持statement(声明)级别
答案解析:
下一题
阐述一下 VUE中 eventbus 的原理
- EventBus是消息传递的一种方式,基于一个消息中心,订阅和发布消息的模式,称为发布订阅者模式
-
- on('name', fn)订阅消息,name:订阅的消息名称, fn: 订阅的消息
-
- emit('name', args)发布消息, name:发布的消息名称 , args:发布的消息
class Bus { constructor () { this.callbacks = {} } $on(name,fn) { this.callbacks[name] = this.callbacks[name] || [] this.callbacks[name].push(fn) } $emit(name,args) { if(this.callbacks[name]){ //存在遍历所有callback this.callbacks[name].forEach(cb => cb(args)) } } }
- 使用
const EventBus = new EventBusClass() EventBus.on('fn1', function(msg) { alert(`订阅的消息是:${msg}`); }); EventBus.emit('fn1', '你好,世界!');
-
下一题
如何在vue项目中实现按需加载?
- Vue UI组件库的按需加载
- 为了快速开发前端项目,经常会引入现成的UI组件库如ElementUI、iView等,但是他们的体积和他们所提供的功能一样,是很庞大的。 而通常情况下,我们仅仅需要少量的几个组件就足够了,但是我们却将庞大的组件库打包到我们的源码中,造成了不必要的开销。
- 不过很多组件库已经提供了现成的解决方案,如Element出品的
babel-plugin-component
和AntDesign出品的babel-plugin-import
安装以上插件后,在.babelrc
配置中或babel-loader
的参数中进行设置,即可实现组件按需加载了。
{ "presets": [["es2015", { "modules": false }]], "plugins": [ [ "component", { "libraryName": "element-ui", "styleLibraryName": "theme-chalk" } ] ] }
- 单页应用的按需加载
- 现在很多前端项目都是通过单页应用的方式开发的,但是随着业务的不断扩展,会面临一个严峻的问题——首次加载的代码量会越来越多,影响用户的体验
- 通过
import(*)
语句来控制加载时机,webpack内置了对于import(*)
的解析,会将import(*)
中引入的模块作为一个新的入口在生成一个chunk。 当代码执行到import(*)
语句时,会去加载Chunk对应生成的文件。import()
会返回一个Promise对象,所以为了让浏览器支持,需要事先注入Promise polyfill
答案解析:
下一题 详述虚拟DOM中的diff算法
- 首先要先讲一下虚拟DOM是如何实现的
- 1.虚拟DOM是通过js语法来在内存中维护一个通过数据解构描述出来的一个模拟DOM树,当数据发生改变的时候,会先对虚拟DOM进行模拟修改,然后在通过新的虚拟DOM树与旧的虚拟DOM树来对比,而这个对比就是通过diff算法来进行的,(JavaScript对象比DOM对象性能高),而在于抽象了DOM的具体实现(对DOM进行了一层抽象)
- 2.接着,来看一下diff算法是如何进行比对的
- 正常的diff算法,是通过层层对比,单单对比来进行的,对于我们的前端性能来说,很明显是不可以这样实现的。
- 前端的虚拟DOM对比
- 正常的diff算法,是通过层层对比,单单对比来进行的,对于我们的前端性能来说,很明显是不可以这样实现的。
- 所以,前端的diff算法是通过以下步骤来实现的
- 步骤一:用JS对象模拟DOM树 步骤二:比较两棵虚拟DOM树的差异 步骤三:把差异应用到真正的DOM树上
- 同时维护新旧两棵虚拟DOM树,当数据发生改变的时候,开始执行对比** 首先对根元素进行对比,如果根元素发生改变就直接对根元素替换 如果根元素没有发生改变的话,再对下一层元素进行对比,如果对比发现元素发生删除,就执行删除,发现元素被替换就执行替换,发现添加了新的元素就执行添加 对比的同时,会通过key值来判断元素是否发生改变,判断元素是仅仅位置发生改变还是需要整个替换或删除 如果不是元素发生改变的话,再对内容进行对比,如果是内容发生改变的话,就直接修改内容 其实就是进行逐层对比,再通过不同的对比来判断执行不同的操作
下一题
什么是服务端渲染(SSR)?谈一谈他的优缺点
服务端渲染:
- 简单理解是将组件或页面通过服务器生成html字符串,再发送到浏览器,当用户第一次请求页面时,客户端拿到手的,是可以直接渲染然后呈现给用户的 HTML 内容,不需要为了生成 DOM 内容自己再去跑一遍 JS 代码。 优点:
- 首屏渲染快
- 利于SEO
- 可以生成缓存片段,生成静态化文件 缺点:
-
- 服务端压力较大
-
- 开发条件受限
-
- 学习成本相对较高
下一题
简单分析一下后端渲染、前端渲染、同构渲染
答案解析:
后端渲染:传统的 ASP、Java 或 PHP 的渲染机制
- 优势:
- 服务端渲染不需要先下载一堆 js 和 css 后才能看到页面(首屏性能)
- SEO
- 服务端渲染不用关心浏览器兼容性问题(随着浏览器发展,这个优点逐渐消失)
- 对于电量不给力的手机或平板,减少在客户端的电量消耗很重要
前端渲染:使用 JS 来渲染页面大部分内容,代表是现在流行的 SPA 单页面应用
- 优势:
- 局部刷新。无需每次都进行完整页面请求
- 懒加载。如在页面初始时只加载可视区域内的数据,滚动后 rp 加载其它数据,可以通过 react-lazyload 实现
- 富交互。使用 JS 实现各种酷炫效果
- 节约服务器成本。省电省钱,JS 支持 CDN 部署,且部署极其简单,只需要服务器支持静态文件即可
- 天生的关注分离设计。服务器来访问数据库提供接口,JS 只关注数据获取和展现
- JS 一次学习,到处使用。可以用来开发 Web、Serve、Mobile、Desktop 类型的应用
同构渲染
同构渲染简单来说就是一份代码,服务端先通过服务端渲染(server-side rendering),生成html以及初始化数据,客户端拿到代码和初始化数据后,通过对html的dom进行patch和事件绑定对dom进行客户端激活(client-side hydration),这个整体的过程叫同构渲染
- 优点
- 有助于 SEO
- 提高首屏性能
- 缺点:
- 性能
- 不容忽视的服务器端和浏览器环境差异
下一题
vue 项目性能优化
- 代码层面
- 合理使用
v-if
和v-show
- 区分
computed
和watch
的使用 v-for
遍历为item
添加key
v-for
遍历避免同时使用v-if
- 通过
addEventListener
添加的事件在组件销毁时要用removeEventListener
手动移除这些事件的监听 - 图片懒加载
- 路由懒加载
- 第三方插件按需引入
SSR
服务端渲染,首屏加载速度快,SEO
效果好
- 合理使用
- Webpack 层面优化
- 对图片进行压缩
- 使用
CommonsChunkPlugin
插件提取公共代码 - 提取组件的 CSS
- 优化
SourceMap
- 构建结果输出分析,利用
webpack-bundle-analyzer
可视化分析工具
答案解析:
下一题
简要说明 sessionStorage、localStorage、cookie 的区别
答案解析:
- cookie 是网站为了标示用户身份而储存在用户本地终端(Client Side)上的数据(通 常经过加密),cookie还可以设置有效时间 cookie 数据始终在同源的 http 请求中携带(即使不需要),记会在浏览器和服务器间 来回传递, 每次 ajax 请求都会把 cookie 传送到后台,cookie 一般用做用户登陆,后台可以根据 cookie 信息判断用户是否登陆状态
- sessionStorage 和 localStorage 不会自动把数据发给服务器,仅在本地保存。
- 区别在于
- 存储大小: cookie 数据大小不能超过 4k。 sessionStorage 和 localStorage 虽然也有存储大小的限制,但比cookie 大得 多,可以达到 5M 或更大。
- 有期时间: localStorage 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据; sessionStorage 数据在当前浏览器窗口关闭后自动删除。 cookie 设置的cookie 过期时间之前一直有效,即使窗口或浏览器关闭