[TOC]

写在前面

微信公众号搜索“燕小书”并关注,获取更多优质文章及最新官方消息,持续更新✌️

正文

1. Form表单是怎么上传文件的?你了解它的原理吗?

简单来说就是把文件转化成字节流,然后使用http进行传输,后端接受后在把二进制转化成原先的文件格式。在HTML表单中,可以上传文件的唯一控件就是<input type="file">。当一个表单包含<input type="file">时,表单的enctype必须指定为multipart/form-data(表明表单需要上传二进制数据),method必须指定为post,浏览器才能正确编码并以multipart/form-data格式发送表单的数据。multiple="multiple"说明可以同时上传多个文件。也可以使用文件编码传输,可以把图片转化成base64格式然后进行传输,到了服务器之后直接解码base64

2. js中定时器误差的原因是什么?怎么解决?

前言

在电商类的项目中,计时器是常见的需求场景,例如商品的倒计时功能。这就需要使用到js的定时器功能,但是众所周知JS的定时器存在误差,并且这种误差会随时间进行累积。这就会导致随着事件的推移实际的显示时间与真实事件发生偏离。

问题复现

//阻塞代码
setInterval(function () {
  var n = 0
  while (n++ < 1000000000);
}, 1000)

var start = new Date().getTime(), count = 0,interval = 1000;
var timer = setTimeout(doFunc,interval);
function doFunc(){
    count++
    console.log(new Date().getTime() - (start + count * 1000) + 'ms');
  if(count < 10){
        timer = setTimeout(doFunc,interval);
    }
}

image.png

由上面实验可以看出,由于阻塞代码的出现,会导致次回调执行时都会存在误差,且误差累计逐渐增大。

原因

一言以蔽之:JS定时器的误差是由于JS的单线程机制和事件轮询机制导致的!

image.png

**简单分析:

由于js的单线机制,当主线程中遇到异步任务(如定时器),会将将其丢到web api中单独的执行,在执行结束后,会将异步任务注册的回调函数丢到异步任务队列(callback queue)中去。然后再主线程同步任务执行结束后,会对callback queue 进行轮询,并执行里面的回调函数。
问题就出现在这里—-主线程执行回调函数的时间是不确定的。我们开发者只能保证定时器结束后 将回调丢到callback queue中的时间,却不能控制主线程执行回调函数的时间。由于项目中总会有一些同步任务,因此定时器回调的执行总是滞后的。**

解决方案

一言以弊之:误差时间补偿

var start = new Date().getTime(), count = 0,interval = 1000;
var offset = 0;//误差时间
var nextTime = interval - offset;//原本间隔时间 - 误差时间
var timer = setTimeout(doFunc,nextTime);
function doFunc(){
    count++
    console.log(new Date().getTime() - (start + count * interval) + 'ms');
    offset = new Date().getTime() - (start + count * interval); // 核心代码
    nextTime = interval - offset;// 核心代码
    if (nextTime < 0) { nextTime = 0; }// 核心代码
  if(count < 10){
        timer = setTimeout(doFunc,nextTime);
    }
}

实验效果:

image.png

主要原理:在前一次回调执行时计算误差时间,并动态的调整 下次 计时器的是时间间隔。
注:这种方案也不能完全消除计时器的误差,例如 若前一次回调的误差过大,大于了设置的时间间隔,则下次计时器的时间间隔会被置为0,也就是立刻执行(只有误差时间小于计时器间隔时才会有调节能力)。

定时器在切换页面时,计时可能会出现问题,我们可以用web worker进行优化,这个后续更新。。。

3. 如何使用promise实现并发数量控制?

需求背景

业务中有个查询操作是根据 机构列表(入参)查询 类目树列表(出参),但是有时机构数量过多,导致查询接口超时,这是我们可以进行分批查询,例如假如入参数量为100,我们可以将其拆分为 10*10进行调用接口,这样可以减轻防止超时。同时由于后端接口有查询限流,因此还需限制并发数量。
核心需求:

  • 分批请求
  • 控制并发数量

    封装类

    ```javascript

// 自定义task 队列 export default class TaskQueue { constructor(props) { this.max = props.max; // 最大并发限制 this.taskList = []; // 任务队列 this.cateTree = []; // 最终的结果列表 this.doneNum = 0; // 已经完成的promise的数量 (用于标识是否成功) this.isFail = false; // 标识是否有请求失败 }

// 任务列表进入队列
addTask(taskList) {
    this.taskList.push(taskList)
}

// 运行函数
run() {
    const that = this;
    const startLength = that.taskList.length;
    return new Promise((resolve, reject) => {
        let result = []; // 保存最终的结果

        function recursion() {
            const length = that.taskList.length;
            if (!length) {
                return;
            }
            // 每次遍历取出指定数量的task并执行
            let min = Math.min(that.max, that.taskList.length);
            // 每次取出后,任务数量减1
            for (let i = 0; i < min; i++) {
                if (that.isFail) {  // 如果有请求失败或者已经完成,则跳出循环  (非最后一个失败)
                    reject(`请求异常,当前共执行请求${that.doneNum}次`)
                    break;
                }
                const task = that.taskList.shift(); // 每次取出队首的task
                that.max--;
                task().then((res) => {
                    that.max++;
                    result.push(res);
                    // 请求全部结束(全部成功)
                    if (++that.doneNum !== startLength) {
                        recursion(); // 递归调用
                    } else {
                        console.log('全部请求结束,共请求', that.doneNum, '次');
                        resolve(result)
                    }
                }).catch(err => {
                    // 请求失败
                    console.log(err, `请求异常,当前共执行请求${that.doneNum}次`);
                    reject(`请求异常,当前共执行请求${that.doneNum}次`)
                    that.isFail = true;
                })
            }
            if (that.isFail) {  // 如果有请求失败或者已经完成,则跳出循环  (最后一个失败)
                reject(`请求异常,当前共执行请求${that.doneNum}次`)
            }
        }
        recursion();
    })
}

}

<a name="jyAEq"></a>
#### 调用
```javascript
 /**
   * @description 获取类目树数据
   */
  fetchCateTree = (props) => {
    const stores=['id1','id2'....]; // 机构列表
    /**
     * @description 根据入参 机构列表 生成promise
     * @param {*} stores   机构列表
     */
    const _createTask = (stores) => {
      return () => {
        return new Promise((resolve, reject) => {
          request({  // request 为请求方法
            storeCodes: (stores || []).map(({ code }) => code).join(','),
          }).then((res) => {
              const list = this.treeMap(res || []);
              resolve(list);
            })
            .catch((e) => {
              reject(e);
            });
        })
      }
    }

    //1、 实例化任务队列
    const taskQueue = new TaskQueue({
      max: 1 // 控制并发请求数量
    });

    // 2、分组后的机构列表
    const groupedList = sliceByLength(newStore, 50);  // sliceByLength--入参切割

    //3、 将分批任务放进队列
    groupedList.map(item => {
      const task = _createTask(item || []);
      taskQueue.addTask(task);
    })
    // 4、运行
    taskQueue.run().then(res => {
     // 接口数据处理
     // console.log(res)
    }).catch(err => console.error(err))
  }

分析

  • 首先关于分批是很好理解的,就是对数据进行切割,每个封装成一个promise,然后将primoise推进promise 队列中。
  • 并发控制:核心思想是按照限定的长度(并发数),逐个将队列中的promise取出并执行,在每个执行时结束后再次从队列中取出一个新的promise执行,直到执行全部结束。

    推荐使用

    https://github.com/sindresorhus/p-limit

    4. 观察下面代码,直接说出输出结果是什么?为什么?

    ```javascript /**
    • 宏任务微任务测试 */ const myTest = async () => { await new Promise(res => setTimeout(() => console.log(111), 1000)); await new Promise(res => setTimeout(() => console.log(222), 2000)); }

**输出结果:111**<br />**原因:这个题还是蛮有迷惑性的,看上去是测试宏任务和微任务的执行顺序,其实根本没有到达那一步,由于第一个await后面的Promise中的没有resolve,所以程序永远不会向下执行,会在输出111,后一直卡在第一个await处。**

> **基础知识补充:**
> - 宏任务:setTimeout,setInterval,Ajax,DOM事件
> - 微任务:Promise,async/await
> 
**微任务会在宏任务之前执行。**
> <br />
> **正常调用顺序:**
> **1、同步调用栈清空**
> **2、执行当前的微任务(轮询微任务队列)**
> **3、尝试渲染DOM **
> **4、触发Event Loop,执行宏任务回调**
> <br />
> **微任务**:由ES6 语法规定(不是W3C规范)。
> **宏任务**:由浏览器决定的。



**进阶一下:下面输出结果是什么?**
```javascript
/**
 * 宏任务微任务测试
 */
const myTest2 = async () => {
    setTimeout(() => console.log('000'), 0);
    await new Promise(res => {
        console.log('555');
        setTimeout(() => console.log(111), 1000);
        res('666')
    });
    await new Promise(res => setTimeout(() => console.log(222), 2000));
    await new Promise(res => setTimeout(() => console.log(333), 0));
}

输出结果:
image.png

5. 对于==隐式转换的理解

? 位置写什么能够才能得到true

var a = ?;

console.log(a == 1 && a == 2 && a == 3);

这里实际上考察的是 == 的阴式转换,具体规则如下:
image.png
由上面可知:对于对象的转化实际上对自身方法的调用,如下:

const obj = {};

console.log(obj.valueOf()); // {}
console.log(obj.tostring()); // '[object Object]'

对于对象原型上面的valueOf方法返回的是他自己,toString方法返回也不合理,我们在这里尝试改写这个对象的valueOf方法(toString方法同理);

const a = {
  n: 1,
  valueOf: function () {
    return this.n++;
  },
};

console.log(a == 1 && a == 2 && a == 3); // true

补充:对于数组来说toString方法就是调用join方法,这里我们试着改写一下数组的toString方法

const a = [1,2,3];
a.join = a.shift;

console.log(a == 1 && a == 2 && a == 3) // true

6. 对闭包的理解

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

闭包有两个常用的用途;

  • 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
  • 闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

比如,函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。

function A() {
  let a = 1
  window.B = function () {
      console.log(a)
  }
}
A()
B() // 1

在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。经典面试题:循环中使用闭包解决 var 定义函数的问题

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

首先因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。
解决办法有三种:

  • 第一种是使用闭包的方式

    for (var i = 1; i <= 5; i++) {
    ;(function(j) {
      setTimeout(function timer() {
        console.log(j)
      }, j * 1000)
    })(i)
    }
    

    在上述代码中,首先使用了立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j 上面不会改变,当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。

  • 第二种就是使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入。

    for (var i = 1; i <= 5; i++) {
    setTimeout(
      function timer(j) {
        console.log(j)
      },
      i * 1000,
      i
    )
    }
    
  • 第三种就是使用 let 定义 i 了来解决问题了,这个也是最为推荐的方式

    for (let i = 1; i <= 5; i++) {
    setTimeout(function timer() {
      console.log(i)
    }, i * 1000)
    }
    

    7. 对于赋值过程的理解

    观察如下代码,探究其输出值

    ```javascript var a = { n: 1 }; var b = a; a.x = a = { n: 2 };

console.log(a.x); // ? console.log(b.x); // ?

这里主要是对js赋值过程的理解,以`a=1`为例,可分为下面四步:

- 找到变量a的内存地址,准备赋值;
- 运算右侧代码,得到准备赋值的数据;
- 将右侧运算的数据放入到之前的地址中;
- 返回整个表达式的结果为右侧运算的数据

知道如上理论之后回到上述代码中逐步运算如下:
```javascript
var a = { n: 1 };
  • 创建变量a,分配地址;
  • 右侧计算返回对象{n: 1};
  • 将对象{n: 1}存入a的地址值指向的内存中。

    var b = a;
    
  • 在内存中创建b变量,并设置b的地址值与a相同(此时a与b指向同一个内存区域)。

    a.x = a = { n: 2 };
    
  • 第一个等号

    • 此时a指向对象{n: 1}上,并不存在属性x,js允许手动创建新属性,创建之后此时a的值为{n: 1, x: undefined},b与a相同
    • 等待等号右边返回值。
  • 第二个等号
    • 找到变量a,重新给a进行赋值;
    • 第二个等号计算右侧返回对象{n: 2},此时a的值为{n: 2},并没有对b进行重新赋值,故b的值为{n: 1, x: undefined}
    • 返回{n: 2}为x属性赋值(这里需要注意的是,x属性定位的时间要先于a重新赋值),此时a = {n: 2}b = {n: 1, x: {n: 2}}

可知答案


console.log(a.x); // undefined
console.log(b.x); // {n: 2}

8. arguments是什么?如何遍历类数组?

arguments是一个对象,它的属性是从 0 开始依次递增的数字,还有calleelength等属性,与数组相似;但是它却没有数组常见的方法属性,如forEach, reduce等,所以叫它们类数组。

要遍历类数组,有三个方法:
(1)将数组的方法应用到类数组上,这时候就可以使用callapply方法,如:

function foo(){ 
  Array.prototype.forEach.call(arguments, a => console.log(a))
}

(2)使用Array.from方法将类数组转化成数组:‌

function foo(){ 
  const arrArgs = Array.from(arguments) 
  arrArgs.forEach(a => console.log(a))
}

(3)使用展开运算符将类数组转化成数组

function foo(){ 
    const arrArgs = [...arguments] 
    arrArgs.forEach(a => console.log(a)) 
}

9. 关于js小数位的转换

观察下面代码,讨论其输出

0.1 + 0.2 === 0.3

分析原因

关于0.1 + 0.2 != 0.3是一个老生常谈的问题,大多数新手前端都知道这个问题。其实这不是js的原因,而是计算机语言的通病,下面探究一下原因。
众所周知,计算机的世界是二进制,而我们的世界是十进制的,就是这个差异导致计算机难以精准执行0.1+0.2的原因

整数部分

对于小数点前的整数部分,计算机的思路是除2取余,实现方法比较简单这里不再赘述

补充:对于任意进制的转换可以通过toString方法轻松实现,Number.prototype.toString()

小数部分

下面介绍小数点后面的小数部分,这也是导致误差的真正原因。对于小数部分来说,计算机转换为二进制的规则是,乘2取整。
以0.125为例

小数 乘积 取整
0.125 * 2 0.25 0
0.25 * 2 0.5 0
0.5 * 2 1 1

即0.125 = 0b0.001。
这样看起来正常转换没有问题啊,接下来以0.2为例:

小数 乘积 取整
0.2 * 2 0.4 0
0.4 * 2 0.8 0
0.8 * 2 1.6 1
0.6 * 2 1.2 1
0.2 * 2 0.4 0
0.4 * 2 0.8 0

我们进入了循环,即0.2 = 0b0.0011001100110011…,结果是无限的,但是计算机储存是有限的只能是取近似值,也就是这个近似值产生了误差,也是导致0.1 + 0.2 != 0.3的原因。

10. 什么是尾调用,使用尾调用有什么好处?

尾调用指的是函数的最后一步调用另一个函数。代码执行是基于执行栈的,所以当在一个函数里调用另一个函数时,会保留当前的执行上下文,然后再新建另外一个执行上下文加入栈中。使用尾调用的话,因为已经是函数的最后一步,所以这时可以不必再保留当前的执行上下文,从而节省了内存,这就是尾调用优化。但是 ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

尾调用的概念非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。

 function f(x){   
   return g(x); 
 }

上面代码中,函数f的最后一步是调用函数g,这就叫尾调用。
以下两种情况,都不属于尾调用。


// 情况一
function f(x){
  let y = g(x);
  return y;
}

// 情况二
function f(x){
  return g(x) + 1;
}

上面代码中,情况一是调用函数g之后,还有别的操作,所以不属于尾调用,即使语义完全一样。情况二也属于调用后还有操作,即使写在一行内。
尾调用不一定出现在函数尾部,只要是最后一步操作即可。


function f(x) {
  if (x > 0) {
    return m(x)
  }
  return n(x);
}

上面代码中,函数m和n都属于尾调用,因为它们都是函数f的最后一步操作。

11. 对于map和parseInt

观察下面函数探究其输出

['1', '2', '3'].map(parseInt);

对于[map](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/map)函数,接受一个callback和一个thisArg(可选)作为参数,其中对于callback接受三个参数,分别是currentValue,index,array,对于map的使用大家都是比较熟悉的,这里不过多的赘述了。
对于[parseInt(string, radix)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt)函数,解析一个字符串并返回指定基数的十进制整数,其中radix是2-36之间的整数,表示被解析字符串的基数。
有了上面的了解,我们就可以把题目中的函数解析成如下:

[parseInt('1', 0), parseInt('2', 1), parseInt('3', 2)]

我们知道radix是介于2-36之间的整数,但是对于1和0的解释要看一下文档的解释。
image.png
这里的情况的0会被当作是10,故parseInt('1', 0) = 1
image.png
这里会直接返回NaN,故parseInt('2', 1) = NaN
对于parseInt(‘3’, 2) 的计算,在二进制中是不会出现3这个数,转换失败,返回NaN,即parseInt('3', 2) = NaN
可见答案

['1', '2', '3'].map(parseInt); // [1, NaN, NaN]

12. for…in和for…of的区别

for…of 是ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,和ES3中的for…in的区别如下

  • for…of 遍历获取的是对象的键值,for…in 获取的是对象的键名;
  • for… in 会遍历对象的整个原型链,性能非常差不推荐使用,而 for … of 只遍历当前对象不会遍历原型链;
  • 对于数组的遍历,for…in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of 只返回数组的下标对应的属性值;

总结:for…in 循环主要是为了遍历对象而生,不适用于遍历数组;for…of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象

13. 可选链

可选链语法?.的三种形式

  • obj?.prop—如果 obj 存在则返回 obj.prop,否则返回 undefined;
  • obj?.[prop]—如果 obj 存在则返回 obj.[prop]否则返回 undefined;
  • obj.method?.()—如果 obj.method 存在则调用 obj.method()否则返回 undefined。

正如我们所看到的,这些语法形式用起来都很简单直接。?. 检查左边部分是否为 null/undefined,如果不是则继续运算。
?. 链使我们能够安全地访问嵌套属性。
但是,我们应该谨慎地使用 ?.,仅在当左边部分不存在也没问题的情况下使用为宜。以保证在代码中有编程上的错误出现时,也不会对我们隐藏。

14. 解释性语言和编译型语言的区别

(1)解释型语言

使用专门的解释器对源程序逐行解释成特定平台的机器码并立即执行。是代码在执行时才被解释器一行行动态翻译和执行,而不是在执行之前就完成翻译。解释型语言不需要事先编译,其直接将源代码解释成机器码并立即执行,所以只要某一平台提供了相应的解释器即可运行该程序。其特点总结如下

  • 解释型语言每次运行都需要将源代码解释称机器码并执行,效率较低;
  • 只要平台提供相应的解释器,就可以运行源代码,所以可以方便源程序移植;
  • JavaScript、Python等属于解释型语言。

    (2)编译型语言

    使用专门的编译器,针对特定的平台,将高级语言源代码一次性的编译成可被该平台硬件执行的机器码,并包装成该平台所能识别的可执行性程序的格式。在编译型语言写的程序执行之前,需要一个专门的编译过程,把源代码编译成机器语言的文件,如exe格式的文件,以后要再运行时,直接使用编译结果即可,如直接运行exe文件。因为只需编译一次,以后运行时不需要编译,所以编译型语言执行效率高。其特点总结如下:

  • 一次性的编译成平台相关的机器语言文件,运行时脱离开发环境,运行效率高;

  • 与特定平台相关,一般无法移植到其他平台;
  • C、C++等属于编译型语言。

两者主要区别在于:前者源程序编译后即可在该平台运行,后者是在运行期间才编译。所以前者运行速度快,后者跨平台性好。

15. 冒泡和捕获

当一个事件发生时 —— 发生该事件的嵌套最深的元素被标记为“目标元素”(event.target)。

  • 然后,事件从文档根节点向下移动到 event.target,并在途中调用分配了 addEventListener(…, true) 的处理程序(true 是 {capture: true} 的一个简写形式)。
  • 然后,在目标元素自身上调用处理程序。
  • 然后,事件从 event.target 冒泡到根,调用使用 on、HTML 特性(attribute)和没有第三个参数的,或者第三个参数为 false/{capture:false} 的 addEventListener 分配的处理程序。

每个处理程序都可以访问 event 对象的属性:

  • event.target —— 引发事件的层级最深的元素。
  • event.currentTarget(=this)—— 处理事件的当前元素(具有处理程序的元素)
  • event.eventPhase —— 当前阶段(capturing=1,target=2,bubbling=3)。

任何事件处理程序都可以通过调用 event.stopPropagation() 来停止事件,但不建议这样做,因为我们不确定是否确实不需要冒泡上来的事件,也许是用于完全不同的事情。
捕获阶段很少使用,通常我们会在冒泡时处理事件。这背后有一个逻辑。
在现实世界中,当事故发生时,当地警方会首先做出反应。他们最了解发生这件事的地方。然后,如果需要,上级主管部门再进行处理。
事件处理程序也是如此。在特定元素上设置处理程序的代码,了解有关该元素最详尽的信息。特定于 的处理程序可能恰好适合于该 ,这个处理程序知道关于该元素的所有信息。所以该处理程序应该首先获得机会。然后,它的直接父元素也了解相关上下文,但了解的内容会少一些,以此类推,直到处理一般性概念并运行最后一个处理程序的最顶部的元素为止。
冒泡和捕获为“事件委托”奠定了基础 —— 一种非常强大的事件处理模式

16. 在一个 DOM 上同时绑定两个点击事件:一个用捕获,一个用冒泡。事件会执行几次?先执行的是冒泡还是捕获?
理解分析

冒泡是从下向上,DOM 元素绑定的事件被触发时,此时该元素为目标元素,目标元素执行后,它的祖先元素绑定的事件会向上顺序执行。addEventListener 函数的第三个参数设置为 false,说明不为捕获事件,即为冒泡事件。
捕获则和冒泡相反,目标元素被触发后,会动目标元素的最顶层祖先元素往下执行到目标元素为止。当一个元素绑定了两个事件,一个是冒泡,一个是捕获。
首先需要明确的是,绑定了几个事件就会执行几次。
对于执行顺序的问题需要注意以下。该 DOM 上的事件如果被触发,会有这几种情况。

  • 如果该 DOM 是目标元素,则按事件绑定顺序执行,不区分冒泡还是捕获
  • 如果该 DOM 是出于事件流中的非目标元素,则先执行捕获后执行冒泡

因为 W3C 标准有说明,先发生捕获事件,后发生冒泡事件。所有事件的顺序是:其它元素捕获阶段事件—-本元素代码顺序事件—-其他元素冒泡阶段事件。需要注意的是:在冒泡阶段,向上执行的过程中,已经执行的捕获事件不再执行,只执行冒泡事件。
【适用于chrom浏览器老版本】


17. 怎么处理项目中的异常捕获行为?

一、代码执行的错误捕获

1.try…………catch

  • 能捕获到代码执行的错误
  • 捕获不到语法的错误
  • 无法处理异步中的错误
  • 使用try……catch包裹,影响代码可读性

2.window.onerror

  • 无论是异步还是非异步错误,onerror都能捕获到运行时错误
  • onerrer主要是来捕获预料之外的错误,而try……catch则是用来在可预见情况下监控特定的错误,两者结合使用更高效
  • window.onerror函数只有在返回true的时候,异常才不会向上抛出,否则即使是知道异常的发生控制台还是会显示:Uncaught Error:xxxxx
  • 当我们遇到报404网络请求异常的时候,onerror是无法帮助我们捕获到异常的

缺点:监听不到资源加载的报错error,事件处理函数只能声明一次,不会重复执行多个回调
3.window.addEventListener(‘error’,function,boolean)
可以监听到资源加载报错,也可以注册多个事件处理函数
window.addEventListener(“error”,(msg,url,row,col,error)=>{},true)
但是这种方式虽然可以捕捉到网络请求的异常,却无法判断HTTP的状态是404还是其他比如500等等,所以还需要配合服务端日志才进行排查分析才可以
4.window.addEventListener(‘unhandledrejection’)
捕获Promise错误,当Promise被reject处理器的时候,会触发unhandledrejection事件;这可能发生在window下,但也可能发生在Worker中。这对于调试回退错误处理非常有用

二、资源加载的错误捕获

  • 1.imgObj.onerror(),图片不存在的时候就会触发onerror事件
  • 2.performance.getEntries(),获取到成功加载的资源,对比可以间接的捕获错误
  • 3.window.addEventListener(‘error’,function,true),会捕获但是不冒泡,所以window.onerror不会触发,捕获阶段可以触发

    18. 强缓存和协商缓存

    1,浏览器进行资源请求时,会判断response headers是否命中强缓存,如果命中,直接从本地读取缓存,不会向服务器发送请求,
    2,当强缓存没有命中时,会发送请求到服务端,判断协商缓存是否命中,如果命中,服务器将请求返回,不会返回资源,告诉浏览器从本地读取缓存。如何不命中,服务器直接返回资源
    区别: 强缓存命中,不会请求服务器,直接请求缓存;协商缓存命中,会请求服务器,不会返回内容,然后读取缓存;
    image.png

    强缓存

    强缓存是利用http的返回头中的Expires或者Cache-Control两个字段来控制的,用来表示资源的缓存时间。
    Expires 该字段是http1.0时的规范,它的值为一个绝对时间的GMT格式的时间字符串,比如Expires:Mon,18 Oct 2066 23:59:59 GMT。这个时间代表着这个资源的失效时间,在此时间之前,即命中缓存。这种方式有一个明显的缺点,由于失效时间是一个绝对时间,所以当服务器与客户端时间偏差较大时,就会导致缓存混乱。
    Cache-Control Cache-Control是http1.1时出现的header信息,主要是利用该字段的max-age值来进行判断,它是一个相对时间,例如Cache-Control:max-age=3600,代表着资源的有效期是3600秒。
    cache-control除了该字段外,还有下面几个比较常用的设置值:
    • no-cache:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。
    • no-store:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。
    • public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器
    • private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。

Cache-Control与Expires可以在服务端配置同时启用,同时启用的时候Cache-Control优先级高。

协商缓存

Last-Modify/If-Modify-Since:浏览器第一次请求一个资源的时候,服务器返回的header中会加上Last-ModifyLast-modify是一个时间标识该资源的最后修改时间;当浏览器再次请求该资源时,request的请求头中会包含If-Modify-Since,该值为缓存之前返回的Last-Modify。服务器收到If-Modify-Since后,根据资源的最后修改时间判断是否命中缓存。
Etag:web服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定)。
If-None-Match:当资源过期时(使用Cache-Control标识的max-age),发现资源具有Etage声明,则再次向web服务器请求时带上头If-None-Match (Etag的值)。web服务器收到请求后发现有头If-None-Match 则与被请求资源的相应校验串进行比对,决定是否命中协商缓存。
关于Etag和Last-Modify

  • Etag要优于Last-Modified。Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;
  • 在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值;
  • 在优先级上,服务器校验优先考虑Etag。

19 ES6模块与CommonJS模块有什么异同?

ES6 Module和CommonJS模块的区别:

  • CommonJS是对模块的浅拷⻉,ES6 Module是对模块的引⽤,即ES6 Module只存只读,不能改变其值,也就是指针指向不能变,类似const;
  • import的接⼝是read-only(只读状态),不能修改其变量值。 即不能修改其变量的指针指向,但可以改变变量内部指针指向,可以对commonJS对重新赋值(改变指针指向),但是对ES6 Module赋值会编译报错。

ES6 Module和CommonJS模块的共同点:

  • CommonJS和ES6 Module都可以对引⼊的对象进⾏赋值,即对对象内部属性的值进⾏改变。

    20 null和undefined的区别

    首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。
    undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化。
    undefined 在 JavaScript 中不是一个保留字,这意味着可以使用 undefined 来作为一个变量名,但是这样的做法是非常危险的,它会影响对 undefined 值的判断。我们可以通过一些方法获得安全的 undefined 值,比如说 void 0
    当对这两种类型使用 typeof 进行判断时,Null 类型化会返回 “object”,这是一个历史遗留的问题。当使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。

21. JavaScript脚本延迟加载的方式有哪些?

延迟加载就是等页面加载完成之后再加载 JavaScript 文件。 js 延迟加载有助于提高页面加载速度。

一般有以下几种方式:

  • defer 属性:给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。
  • async 属性:给 js 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
  • 动态创建 DOM 方式:动态创建 DOM 标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
  • 使用 setTimeout 延迟方法:设置一个定时器来延迟加载js脚本文件
  • 让 JS 最后加载:将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。

    22. 关于NaN

    NaN 指“不是一个数字”(not a number),NaN 是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。
    NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN !== NaN 为 true。 ```javascript typeof NaN; // ‘number’

NaN === NaN; // false

<a name="nkuQ9"></a>
### 23. 箭头函数中的this指向哪里
箭头函数不同于传统JavaScript中的函数,箭头函数并没有属于⾃⼰的this,它所谓的this是捕获其所在上下⽂的 this 值,作为⾃⼰的 this 值,并且由于没有属于⾃⼰的this,所以是不会被new调⽤的,这个所谓的this也不会被改变。<br />这里我们借用Babel来理解箭头函数:
```javascript
// ES6 
const obj = { 
  arrows() { 
    return () => { 
      console.log(this === obj); 
    }; 
  } 
}

转化后

// ES5,由 Babel 转译
var obj = { 
   arrows: function arrows() { 
     var _this = this; 
     return function () { 
        console.log(_this === obj); 
     }; 
   } 
};

24.js中对象创建的方式有哪些?

一般使用字面量的形式直接创建对象,但是这种创建方式对于创建大量相似对象的时候,会产生大量的重复代码。但 js和一般的面向对象的语言不同,在 ES6 之前它没有类的概念。但是可以使用函数来进行模拟,从而产生出可复用的对象创建方式,常见的有以下几种:
(1)第一种是工厂模式,工厂模式的主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的。但是它有一个很大的问题就是创建出来的对象无法和某个类型联系起来,它只是简单的封装了复用代码,而没有建立起对象和类型间的关系。

(2)第二种是构造函数模式。js 中每一个函数都可以作为构造函数,只要一个函数是通过 new 来调用的,那么就可以把它称为构造函数。执行构造函数首先会创建一个对象,然后将对象的原型指向构造函数的 prototype 属性,然后将执行上下文中的 this 指向这个对象,最后再执行整个函数,如果返回值不是对象,则返回新建的对象。因为 this 的值指向了新建的对象,因此可以使用 this 给对象赋值。构造函数模式相对于工厂模式的优点是,所创建的对象和构造函数建立起了联系,因此可以通过原型来识别对象的类型。但是构造函数存在一个缺点就是,造成了不必要的函数对象的创建,因为在 js 中函数也是一个对象,因此如果对象属性中如果包含函数的话,那么每次都会新建一个函数对象,浪费了不必要的内存空间,因为函数是所有的实例都可以通用的。

(3)第三种模式是原型模式,因为每一个函数都有一个 prototype 属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法。因此可以使用原型对象来添加公用属性和方法,从而实现代码的复用。这种方式相对于构造函数模式来说,解决了函数对象的复用问题。但是这种模式也存在一些问题,一个是没有办法通过传入参数来初始化值,另一个是如果存在一个引用类型如 Array 这样的值,那么所有的实例将共享一个对象,一个实例对引用类型值的改变会影响所有的实例。

(4)第四种模式是组合使用构造函数模式和原型模式,这是创建自定义类型的最常见方式。因为构造函数模式和原型模式分开使用都存在一些问题,因此可以组合使用这两种模式,通过构造函数来初始化对象的属性,通过原型对象来实现函数方法的复用。这种方法很好的解决了两种模式单独使用时的缺点,但是有一点不足的就是,因为使用了两种不同的模式,所以对于代码的封装性不够好。

(5)第五种模式是动态原型模式,这一种模式将原型方法赋值的创建过程移动到了构造函数的内部,通过对属性是否存在的判断,可以实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式很好地对上面的混合模式进行了封装。

(6)第六种模式是寄生构造函数模式,这一种模式和工厂模式的实现基本相同,我对这个模式的理解是,它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。它的一个缺点和工厂模式一样,无法实现对象的识别

25. js中判断类型的方法

常见类型

  • 基本类型:string,number,boolean,Symbol;
  • 引用类型:Object,Function,Function,Array,Date,…
  • 特殊类型:undefined,null

    typeof

    typeof '';               // string 
    typeof 1;                // number 
    typeof true;             // boolean 
    typeof undefined;        // undefined 
    typeof null;             // object 
    typeof [] ;              // object 
    typeof new Function();   // function 
    typeof new Date();       // object 
    typeof new RegExp();     // object
    

    在 JavaScript 里 使用 typeof 来判断数据类型,只能区分基本类型,即 “ number ” , “ string ” , “ undefined ” , “ boolean ” , “ object ” 五种。
    对于数组函数对象来说,其关系错综复杂,使用 typeof 都会统一返回 “object” 字符串。
    所以,要想区别对象、数组、函数单纯使用 typeof 是不行的 ,JavaScript 中,通过 Object.prototype.toString 方法,判断某个对象值属于哪种内置类型。

    Object.prototype.toString

    console.log(Object.prototype.toString.call(123))          // [object Number]
    console.log(Object.prototype.toString.call('123'))        // [object String]
    console.log(Object.prototype.toString.call(undefined))    // [object Undefined]
    console.log(Object.prototype.toString.call(true))         // [object Boolean]
    console.log(Object.prototype.toString.call({}))           // [object Object]
    console.log(Object.prototype.toString.call([]))           // [object Array]
    console.log(Object.prototype.toString.call(function(){})) // [object Function]
    console.log(Object.prototype.toString.call(this));        // [object Window]
    

    instanceof

    console.log(2 instanceof Number);                    // false
    console.log(true instanceof Boolean);                // false 
    console.log('str' instanceof String);                // false  
    console.log([] instanceof Array);                    // true
    console.log(function(){} instanceof Function);       // true
    console.log({} instanceof Object);                   // true
    

    constructor

    console.log((2).constructor === Number);                // true
    console.log((true).constructor === Boolean);            // true
    console.log(('str').constructor === String);            // true
    console.log(([]).constructor === Array);                // true
    console.log((function() {}).constructor === Function);  // true
    console.log(({}).constructor === Object);               // true
    

    26. 对执行上下文的理解

    1. 执行上下文类型

    (1)全局执行上下文
    任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文。
    (2)函数执行上下文
    当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。
    (3)**eval**函数执行上下文
    执行在eval函数中的代码会有属于他自己的执行上下文,不过eval函数不常使用,不做介绍。

    2. 执行上下文栈
  • JavaScript引擎使用执行上下文栈来管理执行上下文

  • 当JavaScript执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文。

    let a = 'Hello World!';
    function first() {
    console.log('Inside first function');
    second();
    console.log('Again inside first function');
    }
    function second() {
    console.log('Inside second function');
    }
    first();
    //执行顺序
    //先执行second(),在执行first()
    

    3. 创建执行上下文

    创建执行上下文有两个阶段:创建阶段执行阶段
    1)创建阶段
    (1)this绑定

  • 在全局执行上下文中,this指向全局对象(window对象)

  • 在函数执行上下文中,this指向取决于函数如何调用。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined

(2)创建词法环境组件

  • 词法环境是一种有标识符——变量映射的数据结构,标识符是指变量/函数名,变量是对实际对象或原始数据的引用。
  • 词法环境的内部有两个组件:加粗样式:环境记录器:用来储存变量个函数声明的实际位置外部环境的引用:可以访问父级作用域

(3)创建变量环境组件

  • 变量环境也是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。

2)执行阶段
此阶段会完成对变量的分配,最后执行完代码。

简单来说执行上下文就是指:
在执行一点JS代码之前,需要先解析代码。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。

在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。

  • 全局上下文:变量定义,函数声明
  • 函数上下文:变量定义,函数声明,thisarguments

    27. 原型链的简单理解

  • 每个构造函数都有 prototype 属性,指向原型对象

  • 原型对象有 constructor 指向构造函数
  • 由构造函数创建出来的实例,有 proto 属性指向原型对象
  • 原型对象的 proto 指向原型对象的 proto,最终指向 Object.prototype
  • 构造函数也是对象,他的 proto 指向 Function.prototype
  • Function.proto 指向 Function.prototype
  • Object.proto 指向 Function.prototype
  • Function.prototype.proto 指向 Object.prototype
  • Object.prorotype.proto 指向 null

    28. symbol 的介绍和使用

    symbol 是什么

    Symbol 是 ES6 新推出的一种基本类型,它表示独一无二的值,它可以接受一个字符串作为参数,带有相同参数的两个Symbol值不相等,这个参数只是表示Symbol值的描述而已,主要用于程序调试时的跟踪,当然你也可以不传入参数,同样的我们可以通过typeof来判断是否为Symbol类型。 ```javascript const s1 = Symbol(); const s2 = Symbol(); console.log(s1 === s2); // false

const s1 = Symbol(‘debug’); const str = ‘debug’; const s2 = Symbol(‘debug’); console.log(s1 === str); // false console.log(s1 === s2); // false console.log(s1); // Symbol(debug) console.log(typeof s1); // symbol

<br />Symbol()不是一个完整的构造函数,因此不能通过new Symbol() 来创建(通过 new实例化的结果是一个 object对象,而不是原始类型的 symbol)

```javascript
const s1 = new Symbol();
// Uncaught TypeError: Symbol is not a constructor

Symbol不能进行隐式类型转换,但可以显式转为字符串;不能转化为数字,但可以转化为布尔值

const s = Symbol('s')
// 强制类型转换会抛类型错误

console.log(s + '/s'); // TypeError: Cannot convert a Symbol value to a string
console.log(${s}/s) // TypeError: Cannot convert a Symbol value to a string

// 只能先进行强制转换
console.log(String(s) + '/s'); // Symbol(s)/s
console.log(s.toString() + '/s'); // Symbol(s)/s

Symbols 作为对象的属性


由于Symbol值的唯一性,意味着它可以作为对象的属性名,避免出现相同属性名,产生某一个属性被改写或覆盖的情况。

let sym3 = Symbol('test');
let obj={name: 'lin', [sym3]: 'foo'};
obj[sym3];       //"foo"
JSON.stringify(obj);  //"{"name":"lin"}"
Object.keys(obj);   //["name"]
Object.getOwnPropertyNames(obj);   //["name"]
for (let key in obj) {
console.log(key);     //name
}
Object.getOwnPropertySymbols(obj);  //[Symbol(test)]

Symbol值作为属性名时,需要注意两点: 1、不能通过点运算符访问,需要通过方括号的形式访问。

2、不能通过for…in、for…of遍历,也不会被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但是它也不是私有属性,可以通过Object.getOwnPropertySymbols()和 Reflect.ownKeys()方法获取对象Symbol 属性名。

Symbol的方法


Symbol有两个方法,分别是Symbol.for()和Symbol.keyFor()

1、Symbol.for()


Symbol.for()是用于将描述相同的Symbol变量指向同一个Symbol值

let a1 = Symbol.for('a');
let a2 = Symbol.for('a');
a1 === a2  // true
typeof a1  // "symbol"
typeof a2  // "symbol"

let a3= Symbol("a");
a1 === a3      // false


它跟symbol()的区别是Symbol()定义的值每次都是新建,即使描述相同值也不相等,而Symbol.for()定义的值会先检查给定的描述是否已经存在,如果不存在才会新建一个值,并把这个值登记在全局环境中供搜索,Symbol.for()定义相同描述的值时会被搜索到,描述相同则他们就是同一个值

2、Symbol.keyFor()


Symbol.keyFor()是用来检测该字符串参数作为名称的 Symbol值是否已被登记,返回一个已登记的 Symbol 类型值的key

let a1 = Symbol.for("a");
Symbol.keyFor(a1);    // "a"

let a2 = Symbol("a");
Symbol.keyFor(a2);    // undefined

Symbol的属性

Symbol.prototype.description


Symbol的原型上有一个description属性,用来返回Symbol数据的描述

// Symbol()定义的数据
let a = Symbol("acc");
a.description  // "acc"
Symbol.keyFor(a);  // undefined

// Symbol.for()定义的数据
let a1 = Symbol.for("acc");
a1.description  // "acc"
Symbol.keyFor(a1);  // "acc"

// 未指定描述的数据
let a2 = Symbol();
a2.description  // undefined


description属性和Symbol.keyFor()方法的区别是: description能返回所有Symbol类型数据的描述,而Symbol.keyFor()只能返回Symbol.for()在全局注册过的描述

Symbol的使用场景


1、作为对象属性 当一个复杂对象中含有多个属性的时候,很容易将某个属性名覆盖掉,利用 Symbol 值作为属性名可以很好的避免这一现象

const name = Symbol('name');
const obj = {
[name]: 'ClickPaas',
}


2、ES6 中的类是没有 private 关键字来声明类的私有方法和私有变量的,但是我们可以利用 Symbol 的唯一性来模拟

const speak = Symbol();
class Person {
speak {
console.log(123)
}
}
let person = new Person()
console.log(personspeak)

29. 对rest参数的理解

扩展运算符被用在函数形参上时,它还可以把一个分离的参数序列整合成一个数组

function mutiple(...args) {
  let result = 1;
  for (var val of args) {
    result *= val;
  }
  return result;
}
mutiple(1, 2, 3, 4) // 24

这里,传入 mutiple 的是四个分离的参数,但是如果在 mutiple 函数里尝试输出 args 的值,会发现它是一个数组:

function mutiple(...args) {
  console.log(args)
}
mutiple(1, 2, 3, 4) // [1, 2, 3, 4]

这就是 … rest运算符的又一层威力了,它可以把函数的多个入参收敛进一个数组里。这一点经常用于获取函数的多余参数,或者像上面这样处理函数参数个数不确定的情况。

30. 对JSON的理解

JSON 是一种基于文本的轻量级的数据交换格式。它可以被任何的编程语言读取和作为数据格式来传递。

在项目开发中,使用 JSON 作为前后端数据交换的方式。在前端通过将一个符合 JSON 格式的数据结构序列化为
JSON 字符串,然后将它传递到后端,后端通过 JSON 格式的字符串解析后生成对应的数据结构,以此来实现前后端数据的一个传递。

因为 JSON 的语法是基于 js 的,因此很容易将 JSON 和 js 中的对象弄混,但是应该注意的是 JSON 和 js 中的对象不是一回事,JSON 中对象格式更加严格,比如说在 JSON 中属性值不能为函数,不能出现 NaN 这样的属性值等,因此大多数的 js 对象是不符合 JSON 对象的格式的。

在 js 中提供了两个函数来实现 js 数据结构和 JSON 格式的转换处理,

  • JSON.stringify 函数,通过传入一个符合 JSON 格式的数据结构,将其转换为一个 JSON 字符串。如果传入的数据结构不符合 JSON 格式,那么在序列化的时候会对这些值进行对应的特殊处理,使其符合规范。在前端向后端发送数据时,可以调用这个函数将数据对象转化为 JSON 格式的字符串。
  • JSON.parse() 函数,这个函数用来将 JSON 格式的字符串转换为一个 js 数据结构,如果传入的字符串不是标准的 JSON 格式的字符串的话,将会抛出错误。当从后端接收到 JSON 格式的字符串时,可以通过这个方法来将其解析为一个 js 数据结构,以此来进行数据的访问。

    31. 什么是严格模式

    use strict 是一种 ECMAscript5 添加的(严格模式)运行模式,这种模式使得 Javascript 在更严格的条件下运行。设立严格模式的目的如下:

  • 消除 Javascript 语法的不合理、不严谨之处,减少怪异行为;

  • 消除代码运行的不安全之处,保证代码运行的安全;
  • 提高编译器效率,增加运行速度;
  • 为未来新版本的 Javascript 做好铺垫。

区别:

  • 禁止使用 with语句。
  • 禁止 this 关键字指向全局对象。
  • 对象不能有重名的属性。

    32. babel 的原理

    抽象语法树:在讲babel原理之前先说说什么事抽象语法树( 即AST ),也就是计算机理解我们代码的方式

    console.log("hello")
    

    则会得到这样一个树形结构(已简化):

    {
      "type": "Program", // 程序根节点
      "body": [
          {
              "type": "ExpressionStatement", // 一个语句节点
              "expression": {
                  "type": "CallExpression", // 一个函数调用表达式节点
                  "callee": {
                      "type": "MemberExpression", // 表达式
                      "object": {
                          "type": "Identifier",
                          "name": "console"
                      },
                      "property": {
                          "type": "Identifier",
                          "name": "log"
                      },
                      "computed": false
                  },
                  "arguments": [
                      {
                          "type": "StringLiteral",
                          "extra": {
                              "rawValue": "hello",
                              "raw": "\"hello\""
                          },
                          "value": "hello"
                      }
                  ]
              }
          }
      ],
      "directives": []
    }
    

    babel转化的过程

    javascript基础 - 图9
    1.parse:第一步是babel使用babylon将原始代码转换为抽象语法树
    2.transform:第二步是babel通过babel-traverse对前面的抽象语法树进行遍历修改并获得新的抽象语法树
    3.generator:第三步是babel使用babel-generator将抽象语法树转换为代码
    备注:这三个操作通过babel-core合成一个对外的api供外界使用

    33. 日常前端代码开发中,有哪些值得用 ES6 去改进变成优化或者规范?

  • 常用箭头函数来取代 var self = this;的做法。

  • 常用 let 取代 var 命令。
  • 常用数组/对象的结构赋值来命名变量,结构更清晰,语义更明确,可读性更好。
  • 在长字符串多变量组合场合,用模板字符串来取代字符串累加,能取得更好地效果和阅 读体验。
  • 用 Class 类取代传统的构造函数,来生成实例化对象。
  • 在大型应用开发中,要保持 module 模块化开发思维,分清模块之间的关系,常用 import、 export 方法。

    34 . js 代码编译的基本流程

尽管通常将 JavaScript 归类为“动态”或“解释执行”语言,但事实上它是一门编译语言。 这个事实对你来说可能显而易见,也可能你闻所未闻,取决于你接触过多少编程语言,具 有多少经验。但与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系 统中进行移植。
尽管如此,JavaScript 引擎进行编译的步骤和传统的编译语言非常相似,在某些环节可能 比预想的要杂。
在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编
译”

  • 分词/词法分析(Tokenizing/Lexing)

这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代
码块被称为词法单元(token)。例如,考虑程序 var a = 2;。这段程序通常会被分解成
为下面这些词法单元:var、a、=、2 、;。空格是否会被当作词法单元,取决于空格在
这门语言中是否具有意义。

  • 解析/语法分析(Parsing)

这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法
结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。
var a = 2; 的抽象语法树中可能会有一个叫作 VariableDeclaration 的顶级节点,接下
来是一个叫作 Identifier(它的值是 a)的子节点,以及一个叫作 AssignmentExpression
的子节点。AssignmentExpression 节点有一个叫作 NumericLiteral(它的值是 2)的子
节点。

  • 代码生成

将 AST 转换为可执行代码的过程称被称为代码生成。这个过程与语言、目标平台等息
息相关。
抛开具体细节,简单来说就是有某种方法可以将 var a = 2; 的 AST 转化为一组机器指
令,用来创建一个叫作 a 的变量(包括分配内存等),并将一个值储存在 a 中。

35. Canvas 和 SVG 的区别

  • 从功能上来看,二者都是用来绘制 2D 图形的,但 Canvas 画的是位图,逐像素进行渲染,放大之后会失真,而 SVG 绘制的矢量图其质量不受缩放影响。
  • SVG 支持分层,可以对单独的标签进行修改,Canvas 修改的话,需要将整个画布重新渲染。
  • SVG 节点过多时,渲染速度会减慢,Canvas 性能更好一些,但写起来更复杂。
  • SVG 支持事件处理器,而 Canvas 不支持。
  • SVG 适合带有大型渲染区域的应用程序,不适合游戏;Canvas 适合图像密集型的游戏,因为其中的很多对象会被频繁的重绘。

    36. 了解babel:polyfill、loader、 preset-env及 core之间的关系

    之前在用webpack配置babel解析es6语法时,只是按照文档上的使用说明来配置,并没有去真正了解babel的一系列工具链都是怎么回事,今天做一下回顾:
    我们常接触到的有babelbabel-loader@babel/core@babel/preset-env@babel/polyfill、以及@babel/plugin-transform-runtime,这些都是做什么的?
    1、babel:babel官网对其做了非常明了的定义:

    Babel 是一个工具链,主要用于在旧的浏览器或环境中将 ECMAScript 2015+ 代码转换为向后兼容版本的 JavaScript 代码:
    转换语法
    Polyfill 实现目标环境中缺少的功能 (通过 @babel/polyfill)
    源代码转换 (codemods)
    更多!

我们可以看到,babel是一个包含语法转换等诸多功能的工具链,通过这个工具链的使用可以使低版本的浏览器兼容最新的javascript语法。
需要注意的是,babel也是一个可以安装的包,并且在 webpack 1.x 配置中使用它来作为 loader 的简写 。如:

{
test: /\.js$/,
loader: 'babel',
}

但是这种方式在webpack 2.x以后不再支持并得到错误提示:
The node API forbabelhas been moved tobabel-core
此时删掉 babel包,安装babel-loader, 并制定loader: ‘babel-loader’即可
2、@babel/core:
@babel/core是babel的核心库,所有的核心Api都在这个库里,这些Api供babel-loader调用
3、@babel/preset-env:
这是一个预设的插件集合,包含了一组相关的插件,Bable中是通过各种插件来指导如何进行代码转换。该插件包含所有es6转化为es5的翻译规则
babel官网对此进行的如下说明:

Transformations come in the form of plugins, which are small JavaScript programs that instruct Babel on how to carry out transformations to the code. You can even write your own plugins to apply any transformations you want to your code. To transform ES2015+ syntax into ES5 we can rely on official plugins like@babel/plugin-transform-arrow-functions

大致即es6到es5的语法转换是以插件的形式实现的,可以是自己的插件也可以是官方提供的插件如箭头函数转换插件@babel/plugin-transform-arrow-functions。
由此我们可以看出,我们需要转换哪些新的语法,都可以将相关的插件一一列出,但是这其实非常复杂,因为我们往往需要根据兼容的浏览器的不同版本来确定需要引入哪些插件,为了解决这个问题,babel给我们提供了一个预设插件组,即@babel/preset-env,可以根据选项参数来灵活地决定提供哪些插件。为此,我们往往做如下配置:
image.png
在预设配置中以targets指定了es6向后兼容的浏览器的最低版本,根据兼容的浏览器的最低版本对es6最新语法的支持性提供需要的转换插件。这里的options可以单独放在.babelrc文件中,babel会自动去读取这些选项参数。
4、@babel/polyfill:
@babel/preset-env只是提供了语法转换的规则,但是它并不能弥补浏览器缺失的一些新的功能,如一些内置的方法和对象,如Promise,Array.from等,此时就需要polyfill来做js得垫片,弥补低版本浏览器缺失的这些新功能。
我们需要注意的是,polyfill的体积是很大的,如果我们不做特殊说明,它会把你目标浏览器中缺失的所有的es6的新的功能都做垫片处理。但是我们没有用到的那部分功能的转换其实是无意义的,造成打包后的体积无谓的增大,所以通常,我们会在presets的选项里,配置“useBuiltIns”: “usage”,这样一方面只对使用的新功能做垫片,另一方面,也不需要我们单独引入import ‘@babel/polyfill‘了,它会在使用的地方自动注入。
5、babel-loader:
以上@babel/core、@babel/preset-env 、@babel/polyfill其实都是在做es6的语法转换和弥补缺失的功能,但是当我们在使用webpack打包js时,webpack并不知道应该怎么去调用这些规则去编译js。这时就需要babel-loader了,它作为一个中间桥梁,通过调用babel/core中的api来告诉webpack要如何处理js。
6、@babel/plugin-transform-runtime:
polyfill的垫片是在全局变量上挂载目标浏览器缺失的功能,因此在开发类库,第三方模块或者组件库时,就不能再使用babel-polyfill了,否则可能会造成全局污染,此时应该使用transform-runtime。transform-runtime的转换是非侵入性的,也就是它不会污染你的原有的方法。遇到需要转换的方法它会另起一个名字,否则会直接影响使用库的业务代码,
故开发类库,第三方模块或者组件库时使用transform-runtime,平常的项目使用babel-polyfill即可

37. webpack 的编译流程

  1. 初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置对象
  2. 用上一步得到的参数初始化 Compiler 对象
  3. 加载所有配置的插件
  4. 执行对象的 run 方法开始执行编译
  5. 根据配置中的entry找出入口文件
  6. 从入口文件出发,调用所有配置的Loader对模块进行编译
  7. 找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  8. 根据入口和模块之间的依赖关系组装成一个个包含多个模块的 Chunk
  9. 再把每个 Chunk 转换成一个单独的文件加入到输出列表
  10. 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

    在以上过程中,Webpack 会在特定的时间点广播出特定的事件插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果

特定的时间节点怎么理解?
image.png

38.webpack的插件机制

插件的基本结构:

class HelloPlugin {
  // 在构造函数中获取用户给该插件传入的配置
  constructor(options) {}
  // Webpack 会调用 HelloPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply(compiler) {
    // 在emit阶段插入钩子函数,用于特定时机处理额外的逻辑;
    compiler.hooks.emit.tap('HelloPlugin', (compilation) => {
      // 在功能流程完成后可以调用 webpack 提供的回调函数;
    })
    // 如果事件是异步的,会带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知webpack,才会进入下一个处理流程。
    compiler.plugin('emit', function (compilation, callback) {
      // 支持处理逻辑
      // 处理完毕后执行 callback 以通知 Webpack
      // 如果不执行 callback,运行流程将会一直卡在这不往下执行
      callback()
    })
  }
}

module.exports = HelloPlugin

安装插件时, 只需要将它的一个实例放到Webpack config plugins 数组里面:

const HelloPlugin = require('./hello-plugin.js')
var webpackConfig = {
  plugins: [new HelloPlugin({ options: true })],
}

先来分析一下 webpack Plugin 的工作原理

  1. 读取配置的过程中会先执行 new HelloPlugin(options) 初始化一个 HelloPlugin 获得其实例。
  2. 初始化 compiler 对象后调用 HelloPlugin.apply(compiler) 给插件实例传入 compiler 对象。
  3. 插件实例在获取到 compiler 对象后,就可以通过compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件。
    并且可以通过 compiler 对象去操作 Webpack。

webpack 实现插件机制的大体方式是:

  • 创建 - webpack 在其内部对象上创建各种钩子;
  • 注册 - 插件将自己的方法注册到对应钩子上,交给 webpack;
  • 调用 - webpack 编译过程中,会适时地触发相应钩子,因此也就触发了插件的方法。

webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是 Tapable。
Webpack 的 Tapable 事件流机制保证了插件的有序性,将各个插件串联起来, Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条 webapck 机制中,去改变 webapck 的运作,使得整个系统扩展性良好。
Tapable也是一个小型的 library,是Webpack的一个核心工具。类似于node中的events库,核心原理就是一个订阅发布模式。作用是提供类似的插件接口。
webpack 中最核心的负责编译的Compiler和负责创建 bundles 的Compilation都是 Tapable 的实例,可以直接在 Compiler 和 Compilation 对象上广播和监听事件

39. iframe 的优缺点

优点:

1.iframe能够把嵌入的网页原样展现出来;

2.模块分离,便于更改,如果有多个网页引用iframe,只需要修改iframe的内容,就可以实现调用的每一个页面内容的更改,方便快捷;

3.网页如果为了统一风格,头部和版本都是一样的,就可以写成一个页面,用iframe来嵌套,增加代码的可重用;

4.如果遇到加载缓慢的第三方内容如图标和广告,这些问题可以由iframe来解决;

5.重载页面时不需要重载整个页面,只需要重载页面中的一个框架页;

6.方便制作导航栏。

缺点:

1.样式和脚本需要额外链入,调用外部页面,需要额外调用css,增加页面额外的请求次数,增加服务器的http请求;

2.代码复杂,在网页中使用框架结构最大的弊病是搜索引擎的“蜘蛛”程序无法解读这种页面,会影响搜索引擎优化,不利于网站排名;

3.框架结构有时会让人感到迷惑,滚动条除了会挤占有限的页面空间外会使iframe布局混乱,还会分散访问者的注意力,影响用户体验;

4.链接导航疑问。运用框架结构时,必须保证正确配置所有的导航链接,否则,会给访问者带来很大的麻烦。比如被链接的页面出现在导航框架内,这种情况下访问者便被陷住了,因为此时他没有其他地点可去; 

5.产生多个页面,不易管理;

6.多数小型的移动设备(PDA 手机)无法完全显示框架,设备兼容性差。

40. 简述一下 src 和 href 的区别

src和href都是属于外部资源的引用,像一些图片,css文件,js文件,或者其他的web页面。
src用于替换当前元素, href 用于在当前文档和引用资源之间确立联系。

他们的之间的主要关系可以用这样的一句话来概括:src用于替换这个元素,而href用于建立这个标签与外部资源的关系 href (Hypertext Reference) 表示超文本引用,href这个属性指定web资源的位置,从而定义当前元素或文档与外部资源的联系

  • src:指向外部资源的位置,指向的内容将会嵌入到文档中当前标签所在位置;在请求 src 资源时会将其指向的资源下载并应用到文档中,例如 js 脚本,img 图片和 frame 等元素。类似于将所指向资源嵌入当前标签内。这也是为什么将 js 脚本放在底部而不是头部。
  • href 是 Hypertext Reference 的缩写,指向网络资源所在的位置,建立和当前元素(锚点)或当前文档(链接)之间的链接,如果我们在文档中添加那么浏览器会识别该文档为 css 文件,就会并行下载资源并且不会停止对当前文档的处理。这也是为什么建议使用 link 方式加载 css 文件,而不是 @import 方式

41. CSR 和 SSR 分别是什么?

CSR定义:
CSR全称是 Client Side Rendering ,代表的是客户端渲染。顾名思义,就是在渲染工作在客户端(浏览器)进行,而不是在服务器端进行。举个例子,我们平时用vue,react等框架开发的项目,都是先下载html文档(不是最终的完全的html),然后下载js来执行渲染出页面结果。

客户端渲染的优点:

前后端分离。前端专注于界面开发,后端专注于api开发,且前端有更多的选择性,可以使用vue,react框架开发,而不需要遵循后端特定的模板。

服务器压力变轻了,渲染工作在客户端进行,服务器直接返回不加工的html

用户在后续访问操作体验好,(首屏渲染慢)可以将网站做成SPA,可以增量渲染

客户端渲染渲染的缺点:

不利于SEO,因为搜索引擎不执行JS相关操作,无法获取渲染后的最终html。

首屏渲染时间比较长,因为需要页面执行ajax获取数据来渲染页面,如果请求接口多,不利于首屏渲染

SSR定义:
SSR全称是 Server Side Rendering,代表的是服务端渲染。与客户端渲染不同的是,SSR输出的是一个渲染完成的html,整个渲染过程是在服务器端进行的。例如传统的JSP,PHP都是服务端渲染。

服务端渲染的优点:

有利于SEO,由于页面在服务器生成,搜索引擎直接抓取到最终页面结果。

有利于首屏渲染,html所需要的数据都在服务器处理好,直接生成html,首屏渲染时间变短。

服务端渲染的缺点

占用服务器资源,渲染工作都在服务端渲染

用户体验不好,每次跳转到新页面都需要在重新服务端渲染整个页面,不能只渲染可变区域。

42. 什么是负载均衡

什么是负载均衡?

负载均衡是由多台服务器以对称的方式组成一个服务器集合,每台服务器都具有等价的地位,都可以单独对外供应效力而无须其他服务器的辅助。经过某种负载分管技术,将外部发送来的央求均匀分配到对称结构中的某一台服务器上,而接收到央求的服务器独登时回应客户的央求。均衡负载可以平均分配客户央求到服务器列阵,籍此供应快速获取重要数据,解决很多并发访问效力问题。这种群集技术可以用最少的出资取得接近于大型主机的性能。

负载均衡的原理

系统的扩展可分为纵向(垂直)扩展和横向(水平)扩展。纵向扩展,是从单机的角度通过增加硬件处理能力,比如CPU处理能力,内存容量,磁盘等方面,实现服务器处理能力的提升,不能满足大型分布式系统(网站),大流量,高并发,海量数据的问题。因此需要采用横向扩展的方式,通过添加机器来满足大型网站服务的处理能力。比如:一台机器不能满足,则增加两台或者多台机器,共同承担访问压力。这就是典型的集群和负载均衡架构

负载均衡的类型

根据DNS的负载均衡

经过DNS效力中的随机姓名解析来完结负载均衡,在DNS服务器中,可认为多个不同的地址配置同一个姓名,而最终查询这个姓名的客户机将在解析这个姓名时得到其中一个地址。因此,关于同一个姓名,不同的客户时机得到不同的地址,他们也就访问不同地址上的Web服务器,然后达到负载均衡的目的。

反向署理负载均衡

运用署理服务器可以将央求转发给内部的Web服务器,让署理服务器将央求均匀地转发给多台内部Web服务器之一上,然后达到负载均衡的目的。这种署理方式与一般的署理方式有所不同,标准署理方式是客户运用署理访问多个外部Web服务器,而这种署理方式是多个客户运用它访问内部Web服务器,因此也被称为反向署理模式。Apusic负载均衡器就归于这种类型的。

据NAT的负载均衡技术

网络地址变换为在内部地址和外部地址之间进行变换,以便具备内部地址的计算机能访问外部网络,而当外部网络中的计算机访问地址变换网关拥有的某一外部地址时,地址变换网关能将其转发到一个映射的内部地址上。因此如果地址变换网关能将每个衔接均匀变换为不同的内部服务器地址,尔后外部网络中的计算机就各自与自己变换得到的地址上服务器进行通讯,然后达到负载分管的目的。

负载均衡的好处

高智能化

运用虚拟IP(VIP)地址代表方针服务器和运用,将会话分配到最高可用性的服务器,全程监控每个会话,效力恢复后自动重新挂号,并转发客户机和服务器信息包时供应全地址变换。简略有用的负载均衡算法可以配置包括循环法、最少衔接法、散列法或最少失误法等多种不同的负载均衡方法,也可以对个别服务器配置最大衔接数量阈值和加权值,以防止服务器超载。

高可靠性

架构在专用的高速骨干网之上,该骨干网络供应延迟极小的网络连通性,然后保障GSLB的功能正常发挥和高性能,远远优于根据公网的GSLB。并且,当主站点机房的Internet 出口呈现毛病时,还能将用户自动、透明地从其他分站点Internet入口导向主站点服务器。

高可用性

选用热备份方法,在极短时间内对服务器链路、交换端口和交换机进行检测和毛病转移,使运用免受毛病影响;任何一个服务器或服务器群发生毛病或阻塞,用户将被自动引导到下一个最佳备份服务器或站点,然后更进一步提高了效力和内容的可用性。

43. 说一下对版本控制的理解

一、是什么

版本控制(Version control),是维护工程蓝图的标准作法,能追踪工程蓝图从诞生一直到定案的过程。此外,版本控制也是一种软件工程技巧,借此能在软件开发的过程中,确保由不同人所编辑的同一程序文件都得到同步透过文档控制,能记录任何工程项目内各个模块的改动历程,并为每次改动编上序号版本控制系统在当今的软件开发中,被认为是理所当然的配备工具之一,

二、 分类

根据类别可以分成:本地版本控制系统集中式版本控制系统:SVNTortoiseSVN是一款非常易于使用的跨平台的 版本控制/版本控制/源代码控制软件CVSCVS是版本控制系统,是源配置管理(SCM)的重要组成部分。使用它,您可以记录源文件和文档的历史记录老牌的版本控制系统,它是基于客户端/服务器的行为使得其可容纳多用户,构成网络也很方便这一特性使得CVS成为位于不同地点的人同时处理数据文件(特别是程序的源代码)时的首选分布式版本控制系统GitGit是目前世界上最先进的分布式版本控制系统,旨在快速高效地处理从小型到大型项目的所有事务特性:易于学习,占用内存小,具有闪电般快速的性能使用Git和Gitlab搭建版本控制环境是现在互联网公司最流行的版本控制方式HGMercurial是一个免费的分布式源代码管理工具。它可以有效地处理任何规模的项目,并提供简单直观的界面Mercurial是一种轻量级分布式版本控制系统,采用 Python语言实现,易于学习和使用,扩展性强

三、总结

版本控制系统的优点如下:记录文件所有历史变化,这是版本控制系统的基本能力随时恢复到任意时间点,历史记录功能使我们不怕改错代码了支持多功能并行开发,通常版本控制系统都支持分支,保证了并行开发的可行多人协作并行开发,对于多人协作项目,支持多人协作开发的版本管理将事半功倍

44. rollup 与webpack 的对比

webpack

诞生于2012年,目前Javascript社区使用得比较多的构建工具。它的出现,解决了当时的构建工具不能处理的问题——构建复杂的单页面应用(SPA)。它是一个强力的模块打包器。 所谓包(bundle)就是一个 JavaScript 文件,它把一堆资源(assets)合并在一起,以便它们可以在同一个文件请求中发回给客户端。 包中可以包含 JavaScript、CSS 样式、HTML 以及很多其它类型的文件。

rollup

Rollup是下一代JavaScript模块打包工具。开发者可以在你的应用或库中使用ES2015模块,然后高效地将它们打包成一个单一文件用于浏览器和Node.js使用。 Rollup最令人激动的地方,就是能让打包文件体积很小。这么说很难理解,更详细的解释:相比其他JavaScript打包工具,Rollup总能打出更小,更快的包。因为Rollup基于ES2015模块,比Webpack和Browserify使用的CommonJS模块机制更高效。这也让Rollup从模块中删除无用的代码,即tree-shaking变得更容易。

webpack VS rollup

其实,通过分别对Webpack和Rollup的介绍,不难看出,Webpack和Rollup在不同场景下,都能发挥自身优势作用。Webpack对于代码分割和静态资源导入有着“先天优势”,并且支持热模块替换(HMR),而Rollup并不支持,所以当项目需要用到以上,则可以考虑选择Webpack。但是,Rollup对于代码的Tree-shaking和ES6模块有着算法优势上的支持,若你项目只需要打包出一个简单的bundle包,并是基于ES6模块开发的,可以考虑使用Rollup。
其实Webpack从2.0开始支持Tree-shaking,并在使用babel-loader的情况下支持了es6 module的打包了,实际上,Rollup已经在渐渐地失去了当初的优势了。但是它并没有被抛弃,反而因其简单的API、使用方式被许多库开发者青睐,如React、Vue等,都是使用Rollup作为构建工具的。而Webpack目前在中大型项目中使用得非常广泛。
最后,用一句话概括就是:在开发应用时使用 Webpack,开发库时使用 Rollup

45. js 中数组是怎样存储的

数组的内存模型

Javascript的内存分为堆内存和栈内存,数组作为对象,在建立后存储在堆内存中。
任何计算机语言内存的分配都要经历三个阶段

  • 分配内存
  • 对内存进行读、写
  • 释放内存(垃圾回收)

本文主要针对数组的内存分配进行解释。
Javascript中数组有几个不同于其他语言数组的特点


  • 数组中可以存放不同的数据结构,可以存放数组、对象、Number、Undefined、Null、String、Symbol、Boolean、Function等等。
  • 数组的index是字符串类型的,之所以你可以通过arr[1],获得对应的数据,是因为Javascript自动将数字转化为字符串。

数组本来应该是一个连续的内存分配,但是在Javascript中不是连续分配的,而是类似哈希映射的方式存在的。
对于上述的实现方式,熟悉数据结构的同学应该知道,对于读取操作,哈希表的效率并不高,而修改删除的效率比较高。
现在浏览器为了优化其操作,对数组的创建时候的内存分配进行了优化:


  • 对于同构的数组,也就是,数组中元素类型一致,会创建连续的内存分配
  • 对于不同构数组,按照原来的方式创建。
  • 如果你想插入一个异构数据,那么就会重新解构,通过哈希映射的方式创建

为了进一步优化功能的实现,Javascript中出现了ArrayBuffer,它可以创建连续的内存供编程人员使用。


  • ArrayBuffer是创建一块连续的内存,不能直接操作
  • 通过视图对分配的内存进行读写操作

显而易见,如果通过ArrayBuffer创建的数组进行遍历操作,速度更快。

46. 什么是git stash

含义是将代码暂存起来,让本地仓库回到最后一次提交时的状态,便于代码的管理,主要避免修改文件与最新代码的冲突。 git stash 将当前修改暂存起来 git stash pop 释放之前修改的文件,默认是stash最后一次的内容,之前stash的内容还是存储在git仓库里 git stash save保存的信息,起始和git stash的作用是一样的,塞进的信息便于理解stash条目的内容 git stash list展示指定的stash种的某一条的信息 git stash drop这个命令是要丢弃掉最新的一个stash条目,等同于git stash drop stash@{0} git stash drop stash@{n}这个命令是要丢弃掉指定的一个stash条目 git stash clear 清除所有的stash的信息

47. iframe 安全么?你是怎么理解的?

说明:

  • 嵌入第三方 iframe 会有很多不可控的问题,同时当第三方 iframe 出现问题或是被劫持之后,也会诱发安全性问题。
  • 点击劫持攻击者将目标网站通过 iframe 嵌套的方式嵌入自己的网页种,并将 iframe 设置为透明,诱导用户点击
  • 禁止自己的 iframe 种的连接外部网站的 JS

预防方案:

  • 为 iframe 设置 sandbox 属性,通过它可以对 iframe 的行为进行各种限制,充分实现最小权限原则
  • 服务端设置 X-Frame-Options Header 头,拒绝页面被嵌套,X-Frame-Options 是 HTTP 响应头用来告诉浏览器一个页面是否可以嵌入
  • X-Frame-Options:SAMEORIGIN
  • SAMEORIGIN: iframe页面的地址只能为同源域名下的页面 ALLOW-FROM: 可以嵌套在指定来源的iframe里 DENY: 当前页面不能被嵌套在iframe里 设置 CSP 即 Content-Security-Policy 请求头

    48. JSON 和 XMl 有什么区别

    ``` JSON是一种轻量级的数据交换格式,它完全独立于语言,它基于JavaScript编程语言,易于理解和生成。 XML(可扩展标记语言)旨在传输数据,而不是显示数据。 这是W3C的推荐。可扩展标记语言是一种标记语言,它定义了一组规则,用于以人类可读和机器可读的格式编码文档。 XML的设计目标侧重于internet上的简单性,通用性和可用性。 它是一种文本数据格式,通过Unicode为不同的人类语言提供强大的支持。 尽管XML的设计侧重于文档,但该语言被广泛用于表示任意数据结构,例如web服务中使用的那些数据结构。

JSON是JavaScript Object Notation;XML是可扩展标记语言。 JSON是基于JavaScript语言;XML源自SGML JSON是一种表示对象的方式;XML是一种标记语言,使用标记结构来表示数据项 JSON不提供对命名空间的任何支持;XML支持名称空间 JSON支持数组;XML不支持数组 XML的文件相对难以阅读和解释;与XML相比,JSON的文件非常易于阅读 JSON不使用结束标记;XML有开始和结束标签 JSON的安全性较低;XML比JSON更安全 JSON不支持注释;XML支持注释 JSON仅支持UTF-8编码;XML支持各种编码;


<a name="q1mBj"></a>
### 49通过什么做到并发请求?

```javascript
通过什么做到并发请求?

promise.all  可同时发起多个请求    全部得到返回结果后指向回调


在传统 web 服务器模型中,大多都使用多线程来解决并发问题,因为 I/O 是阻塞的,单线程就意味着用户要等待,显然这是不合理的,所以创建多个线程来响应用户的请求。

JS 是解析性语言,代码按照编码顺序一行一行被压进 stack 里面执行,执行完成之后移除然后继续压下一行代码块进去执行。当主线程接受了 request 后,程序被压进同步执行的 sleep 执行块(假如这里是程序的业务处理),如果在这 10 秒内有第二个 request 进来就会被压进 stack 里面等待 10 秒执行完成之后在进一步处理下一个请求,后面的请求都会被挂起等待前面的同步执行完成之后在执行。

事件驱动可以使单线程的效率高,同时处理数万级的并发而不会造成阻塞。

1. 每个进程只有一个主线程在执行程序代码,形成一个执行栈

2. 主线程之外,还维护一个事件队列。当用户的网络请求或其它的异步操作到来时,都会把它放到事件队列中,此时并不会立即执行它,代码也不会被阻塞,继续往下走,直到主线程代码执行完毕。

3. 主线程代码执行完毕之后,然后通过事件队列机制,开始到事件队列的开头取出第一个事件,从线程池中分配一个线程去执行这个事件,接下来继续取出第二个事件,再从线程池中分配一个线程去执行,然后第三个,第四个。主线程不断地检查事件队列中是否有未执行的事件,直到事件队列中所有事件都执行完毕,此后每当有新的事件加入到事件队列中,都会通知主线程按照顺序取出交给事件循环处理。当有事件执行完毕后,会通知主线程,主线程执行回调,线程池归还给线程池。

4. 主线程不断重复上面的第三步

50. iconfont 有什么优缺点?

优点

  • 可以方便地将任何 CSS 效果应用于它们
  • 因为它们是矢量图形,所以他们是可伸缩的。这意味着我们可以在不降低质量的情况下伸缩它们
  • 我们只需要发送一个或少量 HTTP 请求来加载它们,而不是像图片可能需要多个 HTTP 请求
  • 由于尺寸小,它们加载速度快
  • 它们在所有浏览器中都得到支持,甚至支持到 IE6

不足

  • 不能用来显示复杂图像
  • 通常只限于一种颜色,除非应用一些 css 技巧
  • 字体图标通常是根据特定的网络设计的,例如 1616,3232,4848 等。如果由于某种原因将网格系统改为 2525 可能不会得到清晰的结果

51 . 谈谈对工厂模理解

  • 定义

    工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
    在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
    
  • 主要解决

    主要解决接口选择的问题。
    
  • 应用实例

    您需要一辆汽车,可以直接从工厂里面提货,而不用去管这辆汽车是怎么做出来的,以及这个汽车里面的具体实现。
    Hibernate 换数据库只需换方言和驱动就可以。
    
  • 优点

    一个调用者想创建一个对象,只要知道其名称就可以了。
    扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
    屏蔽产品的具体实现,调用者只关心产品的接口
    
  • 缺点

    每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
    

52. document.write 和 innerHTML 有什么区别?

总概括:document.write会重绘整个页面,而innerHTML是可以重绘页面的某一部分

write是DOM方法,向文档写入HTML表达式或JavaScript代码,可列出多个参数,参数被顺序添加到文档中 ;innerHTML是DOM属性,设置或返回调用元素开始结束标签之间的HTML元素。

两者都可向页面输出内容,innerHTML比document.write更灵活。

当文档加载时调用document.write直接向页面输出内容,文档加载结束后调用document.write输出内容会重写整个页面。通常按照两种的方式使用 write() 方法:一是在使用该方在文档中输出 HTML,二是在调用该方法的的窗口之外的窗口、框架中产生新文档(务必使用close关闭文档)。
在读模式下,innerHTML属性返回与调用元素的所有子节点对应的HTML标记,在写模式下,innerHTML会根据指定的值创建新的DOM树替换调用元素原先的所有子节点。

两者都可动态包含外部资源如JavaScript文件

通过document.write插入