前端 30 问:愿你能三十而立

11,269 阅读29分钟

大家好,我们是高灯科技交易合规前端团队(GFE), 隶属于高灯科技(北京)交易合规业务事业线研发部,是一个富有激情、充满创造力、坚持技术驱动全面成长的团队。在金 9 还剩最后的几天银 10 马上就到来的日子里,如果你正在经历工作调整,那么建议你一定要收藏起来,这是一份预防互联网寒冬的必备指南~

1.输出的结果是什么? 怎么改变循环1输出结果变成循环2的打印结果

for(var i = 0;i<5;i++){
  setTimeout(()=>{
    console.log(i) //五次5
  },1000)
  console.log(i) //0 1 2 3 4
}
for(let i = 0;i<5;i++){
  setTimeout(()=>{
    console.log(i) // 0 1 2 3 4
  },1000)
}
// 改变
for(var i = 0;i<5;i++){
  (function(j) {
      setTimeout( function timer() {
          console.log( j );
      }, j*1000 );
  })(i);
}

这是因为setTimeout是异步执行,每一次for循环的时候,setTimeout都执行一次,但是里面的函数没有被执行,而是被放到了任务队列里,等待执行。只有主线上的任务执行完,才会执行任务队列里的任务。也就是说它会等到for循环全部运行完毕后,才会执行fun函数,但是当for循环结束后此时i的值已经变成了5,因此虽然定时器跑了5秒,控制台上的内容依然是5。

同时因为for循环头部的let不仅将i绑定到for循环中,事实上它将其重新绑定到循环体的每一次迭代中,确保上一次迭代结束的值重新被赋值。setTimeout里面的function()属于一个新的域,通过var定义的变量是无法传入到这个函数执行域中的,通过使用let来声明块变量能作用于这个块,所以function就能使用i这个变量了;这个匿名函数的参数作用域和for参数的作用域不一样,是利用了这一点来完成的。这个匿名函数的作用域有点类似类的属性,是可以被内层方法使用的。

最后呢 使用var声明的变量会存在变量提升的问题,而使用let就不存在变量提升的问题。如果非要使用var当做循环循环头的话,出现循环后打印出的结果一模一样的问题,可以使用传参或者闭包的方式

2.数组循环方式有哪些

(1)、forEach:会遍历数组, 没有返回值, 不允许在循环体内写return, 会改变原来数组的内容.forEach()也可以循环对象

(2)、map:遍历数组, 会返回一个新数组, 不会改变原来数组里的内容

(3)、filter:会过滤掉数组中不满足条件的元素, 把满足条件的元素放到一个新数组中, 不改变原数组

(4)、reduce:

let array = [1, 2, 3, 4];
let temp = array.reduce((x, y) => {
	console.log("x,"+x);
	console.log("y,"+y);
	console.log("x+y,",Number(x)+Number(y));
	return x + y;
});
console.log(temp);  // 10
console.log(array);  // [1, 2, 3, 4]

(5)、every:遍历数组, 每一项都是true, 则返回true, 只要有一个是false, 就返回false

(6)、some:遍历数组的每一项, 有一个返回true, 就停止循环

3.script标签中defer和async的区别?

defer:script被异步加载后并不会⽴刻执⾏,而是要等到整个页面在内存中正常渲染结束后,才会执行。多 个 defer 脚本会按照它们在页面出现的顺序加载。

async:同样是异步加载脚本,区别是脚本加载完毕后⽴即执⾏,这导致async属性下的脚本是乱序的,对于script有先后依赖关系的情况,并不适⽤。

4.用 setTimeout()方法来模拟 setInterval()与 setInterval()之间的什么区别?

首先来看 setInterval 的缺陷,使用 setInterval()创建的定时器确保了定时器代码规则地插 入队列中。这种方式的问题在于,如果定时器代码在代码再次添加到队列之前还没完成执行, 结果就会导致定时器代码连续运行好几次,而之间没有间隔。
所以可能会出现下面的情况:当前执行栈执行的时间很长,导致事件队列里边积累多个定时器加入的事件,当执行栈结束后,这些事件会依次执行而之间没有任何停顿,不能达到间隔一段时间执行的效果。

不过幸运的是:javascript引擎足够聪明,能够避免这个问题。当使用 setInterval()时,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。这确保了定时器代码加入队列中最小的时间间隔为指定时间。这种重复定时器的规则有两个问题:

1.某些间隔会被跳过 ;

2.多个定时器的代码执行时间可能会比预期小 ;

eg: 假设,某个 onclick 事件处理程序使用啦 setInterval()来设置了一个 200ms 的重复定时器。

如果事件处理程序花了 300ms 多一点的时间完成。

注:这个例子中的第 1 个定时器是在 205ms 处添加到队列中的,但是直到过了 300ms 处才能够执行。当执行这个定时器代码时,在 405ms 处又给队列添加了另外一个副本。在下一个间隔,即 605ms 处,第一个定时器代码仍在运行,同时在队列中已经有了一个定时器代码的实例。结果是,在这个时间点上的定时器代码不会被添加到队列中。结果在 5ms 处添加的定时器代码结束之后,405ms 处添加的定时器代码就立刻执行\

所以如果用setTimeout替代的话,就可以确保只有一个事件结束了之后,才会触发下一个定时器事件。这确保了定时器代码加入到队列中的最小时间间隔为指定间隔。

以下是实现的2种简单方式:

function say(){ 
  //something 
  setTimeout(say,200); 
}
setTimeout(say,200) 

// 或者

setTimeout(function(){ 
  //do something 
  setTimeout(arguments.callee,200); 
},200); 

5.JS 怎么控制一次加载一张图片,加载完后再加载下一张

(1)方法 1

<script type="text/javascript"> 
  var obj=new Image(); 
	obj.src="http://www.phpernote.com/uploadfiles/editor/201107240502201179.jpg"; 
	obj.onload=function(){ 
  	alert('图片的宽度为:'+obj.width+';图片的高度为:'+obj.height); 
  	document.getElementById("mypic").innnerHTML="<img src='"+this.src+"' />"; 
	}
</script> 

<div id="mypic">onloading……</div> 

(2)方法 2

<script type="text/javascript"> 
  var obj=new Image(); 
	obj.src="http://www.phpernote.com/uploadfiles/editor/201107240502201179.jpg"; 
	obj.onreadystatechange=function(){ 
  if(this.readyState=="complete"){ 
    alert('图片的宽度为:'+obj.width+';图片的高度为:'+obj.height); 
    document.getElementById("mypic").innnerHTML="<img src='"+this.src+"' />"; 
    }
  }
</script> 

  <div id="mypic">onloading……</div> 

6.如何实现 sleep 的效果(es5 或者 es6)

(1)while 循环的方式

function sleep(ms){ 
  var start=Date.now(),expire=start+ms; 
  while(Date.now()<expire); 
  console.log('1111'); 
  return; 
}

执行 sleep(1000)之后,休眠了 1000ms 之后输出了 1111。上述循环的方式缺点很明显,容易造成死循环。

(2)通过 promise 来实现

function sleep(ms){ 
  var temple=new Promise( 
    (resolve)=>{ 
      console.log(111);
      setTimeout(resolve,ms);
    }); 
  return temple 
}

sleep(500).then(
  function(){
    //console.log(222) 
})

//先输出了 111,延迟 500ms 后输出 222

(3)通过 async 封装

function sleep(ms){ 
  return new Promise((resolve)=>setTimeout(resolve,ms)); 
}
async function test(){ 
  var temple=await sleep(1000); 
  console.log(1111) 
  return temple 
}
test(); 

//延迟 1000ms 输出了 1111

(4).通过 generate 来实现

function* sleep(ms){ 
  yield new Promise(function(resolve,reject){ 
    console.log(111); 
    setTimeout(resolve,ms); 
  })
}

sleep(500).next().value.then(
  function(){
    console.log(2222)
}) 

7.实现 JS 中所有对象的深度克隆(包装对象,Date 对象,正则对象)

通过递归可以简单实现对象的深度克隆,但是这种方法不管是 ES6 还是 ES5 实现,都有 同样的缺陷,就是只能实现特定的 object 的深度复制(比如数组和函数),不能实现包装对象 Number,String ,Boolean,以及 Date 对象,RegExp 对象的复制。

(1)前文的方法

function deepClone(obj){ 
  var newObj= obj instanceof Array?[]:{}; 
  for(var i in obj){ 
    newObj[i] = typeof obj[i] == 'object' ? deepClone(obj[i]):obj[i]; 
  }
  return newObj; 
}

这种方法可以实现一般对象和数组对象的克隆,比如:

var arr=[1,2,3]; 
var newArr=deepClone(arr); 
// newArr->[1,2,3] 
var obj={ 
  x:1, 
  y:2 
}
var newObj=deepClone(obj); 
// newObj={x:1,y:2} 

但是不能实现例如包装对象 Number,String,Boolean,以及正则对象 RegExp 和 Date 对象的

克隆,比如:

//Number 包装对象 
var num=new Number(1); 
typeof num // "object" 
var newNum=deepClone(num); 
//newNum -> {} 空对象 

//String 包装对象 
var str=new String("hello"); 
typeof str //"object" 
var newStr=deepClone(str); 
//newStr-> {0:'h',1:'e',2:'l',3:'l',4:'o'}; 

//Boolean 包装对象 
var bol=new Boolean(true); 
typeof bol //"object" 
var newBol=deepClone(bol); 
// newBol ->{} 空对象  

(2)valueof()函数

所有对象都有 valueOf 方法,valueOf 方法对于:如果存在任意原始值,它就默认将对象转换为表示它的原始值。对象是复合值,而且大多数对象无法真正表示为一个原始值,因此默认的 valueOf()方法简单地返回对象本身,而不是返回一个原始值。数组、函数和正则表达式简单地继承了这个默认方法,调用这些类型的实例的 valueOf()方法只是简单返回这个对象本身。

对于原始值或者包装类:

function baseClone(base){ 
  return base.valueOf(); 
}

//Number 
var num=new Number(1); 
var newNum=baseClone(num); 
//newNum->1 

//String 
var str=new String('hello'); 
var newStr=baseClone(str); 
// newStr->"hello" 

//Boolean 
var bol=new Boolean(true); 
var newBol=baseClone(bol); 
//newBol-> true 

其实对于包装类,完全可以用=号来进行克隆,其实没有深度克隆一说,这里用 valueOf 实现,语法上比较符合规范。

对于 Date 类型:

因为 valueOf 方法,日期类定义的 valueOf()方法会返回它的一个内部表示:1970 年 1 月 1 日以来的毫秒数.因此我们可以在 Date 的原型上定义克隆的方法:

Date.prototype.clone=function(){ 
  return new Date(this.valueOf()); 
}
var date=new Date('2010'); 
var newDate=date.clone(); 
// newDate-> Fri Jan 01 2010 08:00:00 GMT+0800 

对于正则对象 RegExp:

RegExp.prototype.clone = function() { 
  var pattern = this.valueOf(); 
  var flags = ''; 
  flags += pattern.global ? 'g' : ''; 
  flags += pattern.ignoreCase ? 'i' : ''; 
  flags += pattern.multiline ? 'm' : ''; 
  return new RegExp(pattern.source, flags); 
};
var reg=new RegExp('/111/'); 
var newReg=reg.clone(); 
//newReg-> //111// 

8.JS 监听对象属性的改变

我们假设这里有一个 user 对象,

(1)在 ES5 中可以通过 Object.defineProperty 来实现已有属性的监听

Object.defineProperty(user,'name',{ 
	setfunction(key,value){}
})
//缺点:如果 id 不在 user 对象中,则不能监听 id 的变化

(2)在 ES6 中可以通过 Proxy 来实现

var user = new Proxy({},{ 
	set:function(target,key,value,receiver){}
})
//这样即使有属性在 user 中不存在,通过 user.id 来定义也同样可以这样监听这个属性的变化 

9.Vue3.0 里为什么要用 Proxy API 替代 defineProperty API?

答:响应式优化。

(1)defineProperty API 的局限性最大原因是它只能针对单例属性做监听。

Vue2.x 中的响应式实现正是基于 defineProperty 中的 descriptor,对 data 中的属性做了遍历 + 递归,为每个属性设置了 getter、setter。

这也就是为什么 Vue 只能对 data 中预定义过的属性做出响应的原因,在 Vue 中使用下标的方式直接修改属性的值或者添加一个预先不存在的对象属性是无法做到 setter 监听的,这是 defineProperty 的局限性。

(2)Proxy API 的监听是针对一个对象的,这就完全可以代理所有属性。

Proxy API 监听一个对象,那么对这个对象的所有操作会进入监听操作,从而就完全可以代理所有属性,可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写,这样就带来了很大的性能提升和更优的代码。

(3)响应式是惰性的

在 Vue.js 2.x 中,对于一个深层属性嵌套的对象,要劫持它内部深层次的变化,就需要递归遍历这个对象,执行 Object.defineProperty 把每一层对象数据都变成响应式的,这无疑会有很大的性能消耗。

在 Vue.js 3.0 中,使用 Proxy API 并不能监听到对象内部深层次的属性变化,因此它的处理方式是在 getter 中去递归响应式,这样的好处是真正访问到的内部属性才会变成响应式,简单的可以说是按需实现响应式,减少性能消耗。

10、js 中宏观任务、微观任务、循环机制的理解

js 属于单线程,分为同步和异步,js代码是自上向下执行的,在主线程中立即执行的就是同步任务,比如简单的逻辑操作及函数,而异步任务不会立马执行,会挪步放到到异步队列中,比如ajax、promise、事件、计时器等等。

先执行同步,主线程结束后再按照异步的顺序再次执行。

事件循环:同步任务进入主线程,立即执行,执行之后异步任务进入主线程,这样循环。

在事件循环中,每进行一次循环操作称为tick,tick 的任务处理模型是比较复杂的,里边有两个词:分别是 Macro Task (宏任务)和 Micro Task(微任务)

宏观任务和微观任务(先执行微观任务,再执行宏观任务)

针对js

宏观任务:setTimeOut、setTimeInterval、postMessage

微观任务:Promise.then

重点:Promise 同步 Promise.then 异步

console.log("开始111");

setTimeout(function() {

  console.log("setTimeout111");

}, 0);

Promise.resolve().then(function() {

  console.log("promise111");

}).then(function() {

  console.log("promise222");

});

console.log("开始222");

我们按照步骤来分析下:

1、遇到同步任务,直接先打印 “开始111”。

2、遇到异步 setTimeout ,先放到队列中等待执行。

3、遇到了 Promise ,放到等待队列中。

4、遇到同步任务,直接打印 “开始222”。

5、同步执行完,返回执行队列中的代码,从上往下执行,发现有宏观任务 setTimeout 和微观任务 Promise ,那么先执行微观任务,再执行宏观任务。

所以打印的顺序为: 开始111 、开始222 、 promise111 、 promise222 、 setTimeout111 。

11. rgb和16进制颜色转换

首先我们需要知道RGB与十六进制之间的关系,例如我们最常见的白色RGB表示为rgb(255, 255, 255), 十六进制表示为#FFFFFFF, 我们可以把十六进制颜色除‘#’外按两位分割成一部分,即FF,FF,FF, 看一下十六进制的FF转为十进制是多少呢?没错,就是255!

了解了十六进制和RGB关系之后,我们就会发现RGB转十六进制方法就很简单了

将RGB的3个数值分别转为十六进制数,然后拼接,即 rgb(255, 255, 255) => ‘#’ + ‘FF’ + ‘FF’ + ‘FF’。 巧妙利用左移,我们把十六进制数值部分当成一个整数,即FFFFFF,我们可以理解为FF0000 + FF00 + FF, 如同我们上面解释,如果左移是基于十六进制计算的,则可以理解为FF << 4, FF << 2, FF, 而实际上我们转为二进制则变为 FF << 16,如下:

x * 16^4 = x * 2 ^ 16

了解了原理以后,代码如下:

function RGBFUN(rgb){
    // 取出rgb中的数值
    let arr = rgb.match(/\d+/g);
    console.log(arr,'输出rgb转换数值'//[ '0', '255', '0' ] 输出rgb转换数值
    if (!arr || arr.length !== 3) {
        console.error('rgb数值不合法');
        return
    }
    let hex = (arr[0]<<16 | arr[1]<<8 | arr[2]).toString(16);
    // 自动补全第一位
    if (hex.length < 6) {
        hex = '0' + hex;
    }
    console.log(`#${hex}`,'输出转换之后的数值'// #0ff00 输出转换之后的数值
    return `#${hex}`;
}
RGBFUN('rgb(0,255,0)')

12. JavaScript位操作:两个操作数相应的比特位有且只有一个1时,结果为1,否则为0

可以实现简单的加密

const value = 456;
function encryption(str) {
    let s = '';
    str.split('').map(item => {
    s += handle(item);
    })
    return s;
}

function decryption(str) {
let s = '';
str.split('').map(item => {
    s += handle(item);
})
return s;
}

function handle(str) {
    if (/\d/.test(str)) {
    return str ^ value;
    } else {
    let code = str.charCodeAt();
    let newCode = code ^ value;
    return String.fromCharCode(newCode);
    }
}

let init = 'JS 掌握 位运算';
let result = encryption(init);             // ƂƛǨ扄戩Ǩ亅踘穟 加密之后
console.log(result,'加密之后')
let decodeResult = decryption(result);     // JS 掌握 位运算 解密之后
console.log(decodeResult,'解密之后')

13.【基于webpack】有哪些Loader?用过哪些?

文件

  • val-loader:将代码作为模块执行,并将其导出为 JS 代码
  • ref-loader:用于手动建立文件之间的依赖关系

JSON

  • cson-loader:加载并转换 CSON 文件

语法转换

  • babel-loader:使用 Babel 加载 ES2015+ 代码并将其转换为 ES5
  • esbuild-loader:加载 ES2015+ 代码并使用 esbuild 转译到 ES6+
  • buble-loader:使用 Bublé 加载 ES2015+ 代码并将其转换为 ES5
  • traceur-loader:使用 Traceur 加载 ES2015+ 代码并将其转换为 ES5
  • ts-loader:像加载 JavaScript 一样加载 TypeScript 2.0+
  • coffee-loader:像加载 JavaScript 一样加载 CoffeeScript
  • fengari-loader:使用 fengari 加载 Lua 代码
  • elm-webpack-loader:像加载 JavaScript 一样加载 Elm

模板

  • html-loader:将 HTML 导出为字符串,需要传入静态资源的引用路径
  • pug-loader:加载 Pug 和 Jade 模板并返回一个函数
  • markdown-loader:将 Markdown 编译为 HTML
  • react-markdown-loader:使用 markdown-parse 解析器将 Markdown 编译为 React 组件
  • posthtml-loader:使用 PostHTML 加载并转换 HTML 文件
  • handlebars-loader:将 Handlebars 文件编译为 HTML
  • markup-inline-loader:将 SVG/MathML 文件内嵌到 HTML 中。在将图标字体或 CSS 动画应用于 SVG 时,此功能非常实用。
  • twig-loader:编译 Twig 模板并返回一个函数
  • remark-loader:通过 remark 加载 markdown,且支持解析内容中的图片

样式

  • style-loader:将模块导出的内容作为样式并添加到 DOM 中
  • css-loader:加载 CSS 文件并解析 import 的 CSS 文件,最终返回 CSS 代码
  • less-loader:加载并编译 LESS 文件
  • sass-loader:加载并编译 SASS/SCSS 文件
  • postcss-loader:使用 PostCSS 加载并转换 CSS/SSS 文件
  • stylus-loader:加载并编译 Stylus 文件

框架

  • vue-loader:加载并编译 Vue 组件
  • angular2-template-loader:加载并编译 Angular 组件

14.谈谈Js如何实现继承?

1.原型链继承:将父类的实例作为子类的原型

优点:

  1. 简单,易于实现

  2. 父类新增原型方法、原型属性,子类都能访问到

缺点:

  1. 无法实现多继承,因为原型一次只能被一个实例更改

  2. 来自原型对象的所有属性被所有实例共享(上诉例子中的color属性)

  3. 创建子类实例时,无法向父构造函数传参

2.构造函数继承:复制父类的实例属性给子类

优点:

  1. 解决了原型链继承中子类实例共享父类引用属性的问题

  2. 创建子类实例时,可以向父类传递参数

  3. 可以实现多继承(call多个父类对象)

缺点:

  1. 实例并不是父类的实例,只是子类的实例

  2. 只能继承父类实例的属性和方法,不能继承其原型上的属性和方法

  3. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

3.组合继承(经典继承):将原型链和借用构造函数的技术组合到一块。使用原型链实现对原型属性和方法的继承,而通过构造函数来实现对实例属性的继承

优点:

  1. 弥补了构造继承的缺点,现在既可以继承实例的属性和方法,也可以继承原型的属性和方法

  2. 既是子类的实例,也是父类的实例

  3. 可以向父类传递参数

  4. 函数可以复用

缺点:

  1. 调用了两次父类构造函数,生成了两份实例

  2. constructor指向问题

4.实例继承:为父类实例添加新特征,作为子类实例返回

优点:

  1. 不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果

缺点:

  1. 实例是父类的实例,不是子类的实例

  2. 不支持多继承

5.拷贝继承:对父类实例中的的方法与属性拷贝给子类的原型

优点:

  1. 支持多继承

缺点:

  1. 效率低,性能差,占用内存高(因为需要拷贝父类属性)

  2. 无法获取父类不可枚举的方法(不可枚举的方法,不能使用for-in访问到)

6.寄生组合继承:通过寄生方式,砍掉父类的实例属性,避免了组合继承生成两份实例的缺点

优点:

  1. 比较完美(js实现继承首选方式)

缺点:

1.实现起来较为复杂(可通过Object.create简化)

7.es6--Class继承:使用extends表明继承自哪个父类,并且在子类构造函数中必须调用super

优点:

1.语法简单易懂,操作更方便

缺点:

1.并不是所有的浏览器都支持class关键字 lass Per

15.最终结果会输出什么?

var length = 10;
function fn(){
    console.log(this.length)
}
var obj = {
    length: 5,
    method: function(fn){
        fn();
        arguments[0]()
    }
}
obj.method(fn,1)			//10  2

解析:

1、fn() :任意函数里如果嵌套了非箭头函数,那这个时候,嵌套函数里的 this 在未指定的情况下,应该指向的是 window 对象,所以这里执行 fn 会打印 window.length。如果使用 let 声明变量时会形成块级作用于,且不存在变量提升;而 var 存在声明提升。所以是输出结果是10;但是如果是let length = 10;那输出结果就是:0 2。

2、arguments0 :在方法调用(如果某个对象的属性是函数,这个属性就叫方法,调用这个属性,就叫方法调用)中,执行函数体的时候,作为属性访问主体的对象和数组便是其调用方法内 this 的指向。(通俗的说,调用谁的方法 this 就指向谁)。

这里 arguments0 是作为 arguments 对象的属性 [0] 来调用 fn 的,所以 fn 中的 this 指向属性访问主体的对象 arguments。

这里还有一点要注意,arguments 这个参数,指的是 function 的所有实参,与形参无关。也就是调用函数时,实际传入的参数,fn 与 1 作为一个参数存储在 arguments 对象里,所以 arguments 的长度为2,所以输出 2。

16. 如何实现CLI创建模板项目?

  1. 讲述实现的基本步骤

    1. 终端参数解析
    2. CLI交互收集创建项目必要参数
    3. 拷贝模板项目
    4. 重写package.json文件(修改项目名称等)
    5. 创建完成后提示或执行项目的初始化过程
  2. 讲述实现过程的注意事项

    1. package.json中name的命名规范
    2. 同名项目再次被创建需要考虑是否覆盖或中断,覆盖的情况下要考虑已创建的项目是否接入的版本管理
    3. 创建完成后提示或执行项目初始化要根据使用时的包管理器来判断
  3. 内容扩展

    1. 在CLI交互收集参数可以支持历史项目模板或第三方模板,通过对应模板的创建方式来实现兼容
    2. CLI创建项目通常要先执行安装CLI的命令,再来运行CLI内置的创建项目命令两步,可以考虑使用命名规约来实现直接执行npm create xxx、yarn create xxx 启动CLI创建项目

17. 如何实现自动化埋点?

自动化埋点可以利用 Babel + 定制埋点插件在项目编译过程中对函数插桩实现埋点自动化,使用Babel自动化插桩需要关注两点:

  1. 埋点模块判断是否已经导入和如何导入:

    1. 在 Babel 插件执行中通过遍历代码中的ImportDeclaration来分析是否包含了指定的埋点模块;
    2. 通过Babel内置@babel/helper-module-imports模块来实现埋点模块导入;
  2. 埋点函数如何生成并插入到源函数;

    1. 如何生成:通过内置APItemplate.statement来生成调用埋点函数的AST;
    2. 如何插入:函数的不同形式对应的AST中类型包括了ClassMethodArrowFunctionExpressionFunctionExpressionFunctionDeclaration,在 Babel 遍历它们的时候在对应的节点的body中插入生成的AST,当遇到没有函数体的函数需要增加函数体并处理返回值;

18. 讲述一下JSON Web Token?

  1. 工作原理:

JWT在服务器认证后生成包含用户信息,时间时间,安全信息等内容组成的JSON对象来唯一表示当前用户状态,在其后的数据交互中持续携带来表明请求的有效性。

  1. 数据结构:

    JWT是有header,payload和signature三部分通过“.”连接起来的字符串,在JWT字符串中没有换行

    1. header是一个JSON对象,用来描述一些元数据;
    2. payload的格式要求同header,内容主要官方定义字段+自定义的字段来满足业务场景的需要;
    3. “Header”和“Payload”都没有提到加密一说,只是进行的字符的编码,所以在“Header”和“Payload”中我们不应该放置一些用户相关的涉及安全的信息,未防止上述两块的内容被中间商拦截篡改,所以需要用到“Signature”;
signature = HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)
  1. 使用方式:

标准的使用方式为在HTTP的头部增加key为“Authorization”,value为:“ Bearer ”的一组信息,token的具体存储按实际业务处理。

  1. 注意事项:

    1. JWT默认不加密,但可以自行加密处理。
    2. 采用默认不加密的情况,请勿将涉密数据放入JWT中。
    3. 建议采用HTTPS来防止中间人攻击。

19. JavaScript中toString()和join()的区别?

// 声明两个不同的数组
const fruitList = ['苹果', '香蕉', '百香果']
const objList = [
  {
    name: '张三',
    age: 16
  },
  {
    name: '李斯',
    age: 18    
  }
]
// 通过toString()直接进行转换为字符串
console.log(fruitList.toString()) // '苹果,香蕉,百香果'
console.log(objList.toString()) // '[object Object],[object Object]'

// 通过join()自定义拼接进行转换为字符串
console.log(fruitList.join()) // '苹果,香蕉,百香果'
console.log(fruitList.join(',')) // '苹果,香蕉,百香果'
console.log(fruitList.join('、')) // '苹果、香蕉、百香果'
console.log(fruitList.join('分割')) // '苹果分割香蕉分割百香果'
console.log(objList.join()) // '[object Object],[object Object]'

总结结论:

toString()与join()是将数组中的所有元素放入一个字符串。

当join()函数内部参数为空时,join()等价于toString()。

当join(‘,’)函数内部参数为逗号字符,则表示用逗号分割数组,等价于内部参数为空。

当join(‘、’)函数内部参数为顿号字符,则表示用顿号分割数组。

当join(‘分割’)函数内部参数为字符串时,则表示用‘分割’字符串分割数组。

20. null / undefined 的区别

null值:属于null类型,代表“空值",代表一个空对象指针;使用typeof运算得到 “object",所以你可以认为它是一个特殊的对象值。

undefined值:属于undefined类型,当一个声明的变量未初始化赋值时,得到的就是undefined。使用typeof运算得到“undefined"。

21. 循环遍历map()与forEach()的区别?

array.map(function(currentValue, index, arr), thisValue)
const nums = [4, 9, 16, 25]
const emptyList = []
const doubleNums = nums.map(Math.sqrt)

console.log(doubleNums) // [2, 3, 4, 5]
console.log(nums) // [4, 9, 16, 25]
console.log(emptyList.map(Math.sqrt)) // []

定义和用法:

map()方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。

map()方法按照原始数组元素顺序依次处理元素。

map()不会对空数组进行检测。

map()不会改变原始数组。

array.forEach(function(currentValue, index, arr), thisValue)
const nums = [4, 9, 16, 25]
const emptyList = []
const doubleNums = nums.forEach(Math.sqrt)

console.log(doubleNums) // undefined
console.log(emptyList.forEach(Math.sqrt)) // undefined

定义和用法:

forEach()方法用于调用数组的每个元素,并将元素传递给回调函数。

forEach()对于空数组是不会执行回调函数的。

区别:

forEach()方法不会返回执行结果,而是undefined。也就是说,forEach()会修改原来的数组。而map()方法会得到一个新的数组并返回。

map()会分配内存空间存储新数组并返回,forEach()不会返回数据。

forEach()允许callback更改原始数组的元素。map()返回新的数组。

22. for/in 与 for/of有什么区别?

Array.prototype.method = function () {}
let myArray = [1, 2, 3]
myArray.name = '数组';

for (let index in myArray) {
  console.log(index) // 0,1,2, name, method
  console.log(myArray[index]) //1,2,3,数组,f() {}
}

for (let val of myArray) {
  console.log(val) // 1,2,3
}

最直接的区别就是: for/in遍历的是数组的索引,而for/of遍历的是数组元素值

for/in遍历的缺陷:

  1. 索引是字符串类型的数字,所以不能直接进行几何运算
  2. 遍历顺序可能不是实际的内部顺序
  3. for/in会遍历数组所有的可枚举属性,包括原型。

所以一般用来遍历对象,而不是数组。

for/of遍历的缺陷:

for/of不支持普通对象,想遍历对象的属性,可以用for/in循环,或者内建的Object.keys()方法:

Object.keys(obj)获取对象的实例属性组成的数组,不包括原型方法和属性

for(let key of Object.keys(obj)) {
  console.log(key + ":" + Object[key])
}

for/of语句创建一个循环来迭代可迭代的对象,允许遍历Arrays,Strings,Map, Set等可迭代的数据结构

// variable 每个迭代的属性值被分配给该变量。
// iterable 一个具有可枚举属性并且可以迭代的对象。
for(let variable of iterable) {
  statement
}

总体来说:for/in遍历对象;for/of遍历数组

23.css 的渲染层合成是什么,浏览器如何创建新的渲染层

在 DOM 树中每个节点都会对应一个渲染对象(RenderObject),当它们的渲染对象处于相同的坐标空间(z 轴空间)时,就会形成一个 RenderLayers,也就是渲染层。渲染层将保证页面元素以正确的顺序堆叠,这时候就会出现层合成(composite),从而正确处理透明元素和重叠元素的显示。对于有位置重叠的元素的页面,这个过程尤其重要,因为一旦图层的合并顺序出错,将会导致元素显示异常。

浏览器如何创建新的渲染层

  • 根元素 document
  • 有明确的定位属性(relative、fixed、sticky、absolute)
  • opacity < 1
  • 有 CSS fliter 属性
  • 有 CSS mask 属性
  • 有 CSS mix-blend-mode 属性且值不为 normal
  • 有 CSS transform 属性且值不为 none
  • backface-visibility 属性为 hidden
  • 有 CSS reflection 属性
  • 有 CSS column-count 属性且值不为 auto 或者有 CSS column-width 属性且值不为 auto
  • 当前有对于 opacity、transform、fliter、backdrop-filter 应用动画
  • overflow 不为 visible

24.路由原理 history 和 hash 两种路由方式的特点

hash 模式

  1. location.hash 的值实际就是 URL 中#后面的东西 它的特点在于:hash 虽然出现 URL 中,但不会被包含在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。
  2. 可以为 hash 的改变添加监听事件

window.addEventListener("hashchange", funcRef, false);

每一次改变 hash(window.location.hash),都会在浏览器的访问历史中增加一个记录利用 hash 的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了

特点:兼容性好但是不美观

history 模式

利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。

这两个方法应用于浏览器的历史记录站,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。

特点:虽然美观,但是刷新会出现 404 需要后端进行配置

25.XMLHttpRequest与fetch的区别?

(1).XMLHttpRequest 对象在所有现代的浏览器都支持,Fetch 是一种新的原生 JavaScript API,目前大多数浏览器都支持

(2).XMLHttpRequest基于onreadystatechange执行回调,Fetch API 使用 Promise,避免了回调。

(3).Fetch只对网络请求报错,对400,500都当做成功的请求,需要封装去处理。

(4).Fetch 请求默认是不带 cookie 的,需要设置 fetch(url, {credentials: 'include'})。

(5).Fetch不支持abort,因为Fetch返回的是一个promise,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了量的浪费。

(6).fetch() 示例中,可以传递字符串定义 的URL 端点,也可以传递一个可配置的 Request 对象。

(7).在 XMLHttpRequest 中管理缓存很麻烦,Fetch 对象中内置了对缓存的支持:

(8).跨域请求中,Fetch 提供了一个模式属性,可以在参数中设置‘no-cors’属性。

(9).默认情况下,fetch() 和 XMLHttpRequest 都遵循服务器重定向。但是,fetch() 在参数中提供了可选项:

redirect

'follow' —— 遵循所有重定向(默认)

'error' —— 发生重定向时中止(拒绝)

'manual' —— 返回手动处理的响应

(10).XMLHttpRequest 将整个响应读入内存缓冲区,但是 fetch() 可以流式传输请求和响应数据,这是一项新技术,流允许你在发送或接收时处理更小的数据块。

(11).Deno 和 Node 中完全支持 Fetch,XMLHttpRequest 只在客户端被支持。

26.前端如何实现大文件下载?

(1).后端服务支持的情况下可以使用分块下载。

(2).云上文件可以采用a标签方式,通过浏览器默认机制实现下载

(3).借助StreamAPI与Service Worker实现

27.常见几种前端微服务及实现原理

微前端架构将后端微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。

微前端架构一般可以由以下几种方式进行:

  1. 路由分发

通过路由将不同的业务分发到不同的、独立前端应用上。其通常可以通过 HTTP 服务器的反向代理来实现,又或者是应用框架自带的路由来解决。

  1. 通过iframe容器

使用iframe创建一个全新的独立的宿主环境,运行各自独立的前端应用

  1. 前端微服务化

在页面合适的地方引入或者创建 DOM,用户访问到对应URL时,加载对应的应用,并能卸载应用

  1. 应用微件化

也叫组合式集成,即通过软件工程的方式在构建前、构建时、构建后等步骤中,对应用进行一步的拆分,并重新组合

  1. 基于Web Components

使用 Web Components 技术构建独立于框架的组件,随后在对应的框架中引入这些组件

28.前端部署都有哪些方案?

前端部署的方式大致有三种,

  • 使用Nginx,Tomcat,IIS等web服务器部署
  • Dokcer部署
  • OSS+CDN 部署,或COS

29.常见发布方案有几种

  1. 蓝绿发布

蓝绿发布是指发布过程中新应用发布测试通过后,通过切换网关流量, 一键升级应用的发布方式

  1. 滚动发布

一般是停止一个或多个服务,执行更新,并重新将其投入使用,周而复始,直到微服务中所有的因公都更新成新版本。

  1. 灰度发布

是指在黑与白之间,能够平滑过渡的一种发布方式。AB test就是一种灰度发布方式,让一部分用户继续用A,一部分用户开始用B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度,而我们平常所说的金丝雀部署也就是灰度发布的一种方式。

30.如何设计一套A/B方案

A/B发布是一种比较高级的流量分配方案,例如根据 URL 是否匹配请求不同的版本应用,或者根据 Header 下某个自定义参数进行用户的区分,从而请求不同的版本应用

涉及流量管理策略,Istio ,docker,k8s等技术

当前微服务、云原生的理念逐渐深入,现有前端的开发方式和前端架构也将跟随着不断变化,利用好以上技术能够实现更迅速地扩展,更稳定地交付,应用之间的联系也会愈加紧密,未来几年的前端也必将是微服务化的时代。

结语:

看到这里不知道你有没有得到收获呢?如果你还有更多的抗冻指南欢迎你在评论区留言,分享给更多的JY,如果上面的内容你还要问题也可以留言交流,愿每一次的互联网寒冬都打不倒任何一个有准备的JY~