前端JavaScript面试

js 高频面试题(最新)

2019-11-22  本文已影响0人  北冥有鱼_425c

1、深浅拷贝

(1) 定义

浅拷贝: 将原对象或原数组的引用直接赋给新对象,新数组,新对象/数组只是原对象的一个引用

深拷贝: 创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”

(2) 浅拷贝

下面这段代码就是浅拷贝,有时候我们只是想备份数组,但是只是简单让它赋给一个变量,改变其中一个,另外一个就紧跟着改变,但很多时候这不是我们想要的

var obj = {
    name:'wsscat',
    age:0
}
var obj2 = obj;
obj2['c'] = 5;
console.log(obj);//Object {name: "wsscat", age: 0, c: 5}
console.log(obj2);////Object {name: "wsscat", age: 0, c: 5}

var arr1 = [1,2,3,4];
var arr2 = arr1;
arr2.push(5);
console.log(arr1); // [1,2,3,4,5] 
console.log(arr2); // [1,2,3,4,5] 

(3) 深拷贝(只拷贝第一层)

拷贝数组

// 使用slice实现
var arr = ['wsscat', 'autumns', 'winds'];
var arrCopy = arr.slice(0);
arrCopy[0] = 'tacssw'
console.log(arr)//['wsscat', 'autumns', 'winds']
console.log(arrCopy)//['tacssw', 'autumns', 'winds']

// 使用concat实现
var arr = ['wsscat', 'autumns', 'winds'];
var arrCopy = arr.concat();
arrCopy[0] = 'tacssw'
console.log(arr)//['wsscat', 'autumns', 'winds']
console.log(arrCopy)//['tacssw', 'autumns', 'winds']

// 使用扩展运算符...
var arr1 = [1,2,3,4];
var arr2 = [...arr1];

拷贝对象

// 遍历属性并赋给新对象
var obj = {
    name:'wsscat',
    age:0
}

var obj2 = new Object();
obj2.name = obj.name;
obj2.age = obj.age

obj.name = 'autumns';
console.log(obj);//Object {name: "autumns", age: 0}
console.log(obj2);//Object {name: "wsscat", age: 0}


// es6的Object.assign方法
// Object.assign:用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target),并返回合并后的target
var obj = {
  name: '彭湖湾',
  job: '学生'
}
var copyObj = Object.assign({}, obj);


// 扩展运算符
let obj1 = {a:2, b:3};
let obj2 = {...obj1};

(4) 深拷贝(拷贝所有层级)

方法一: 封装一个方法来处理对象的深拷贝,代码如下:

var obj = {
    name: 'wsscat',
    age: 0
}
var deepCopy = function(source) {
    var result = {};
    for(var key in source) {
        if(typeof source[key] === 'object') {
            result[key] = deepCopy(source[key])
        } else {
            result[key] = source[key]
        }
    }
    return result;
}
var obj3 = deepCopy(obj)
obj.name = 'autumns';
console.log(obj);//Object {name: "autumns", age: 0}
console.log(obj3);//Object {name: "wsscat", age: 0}

方法二: 使用JSON

var obj = {
  a:2,
  b:3,
  o: {
    x: 100
  }
}

var objStr = JSON.stringify(obj);
var obj2 = JSON.parse(objStr);
obj2.o.x = 1000;
consolo.log(obj2.o.x); // 1000
consolo.log(obj.o.x); // 100

2.typeof运算符和instanceof运算符以及isPrototypeOf()方法的区别

typeof是一个运算符,用于检测数据的类型,比如基本数据类型null、undefined、string、number、boolean,以及引用数据类型object、function,但是对于正则表达式、日期、数组这些引用数据类型,它会全部识别为object; instanceof同样也是一个运算符,它就能很好识别数据具体是哪一种引用类型。它与isPrototypeOf的区别就是它是用来检测构造函数的原型是否存在于指定对象的原型链当中;而isPrototypeOf是用来检测调用此方法的对象是否存在于指定对象的原型链中,所以本质上就是检测目标不同。

3.什么是事件代理/事件委托?

事件代理/事件委托是利用事件冒泡的特性,将本应该绑定在多个元素上的事件绑定在他们的祖先元素上,尤其在动态添加子元素的时候,可以非常方便的提高程序性能,减小内存空间。

4.什么是事件冒泡?什么是事件捕获?

冒泡型事件:事件按照从最特定的事件目标到最不特定的事件目标(document对象)的顺序触发。

捕获型事件:事件从最不精确的对象(document 对象)开始触发,然后到最精确(也可以在窗口级别捕获事件,不过必须由开发人员特别指定)。

5.请指出document.onload和document.ready两个事件的区别

页面加载完成有两种事件,一是ready,表示文档结构已经加载完成(不包含图片等非文字媒体文件),二是onload,指示页面包含图片等文件在内的所有元素都加载完成。

6.如何从浏览器的URL中获取查询字符串参数?

getUrlParam : function(name){
        //baidu.com/product/list?keyword=XXX&page=1
        var reg     = new RegExp('(^|&)' + name + '=([^&]*)(&|$)');
        var result  = window.location.search.substr(1).match(reg);
        return result ? decodeURIComponent(result[2]) : null;
    }

7.什么是"use strict";?使用它的好处和坏处分别是什么?

在代码中出现表达式-"use strict"; 意味着代码按照严格模式解析,这种模式使得Javascript在更严格的条件下运行。

好处:

坏处:

8.请解释JSONP的工作原理,以及它为什么不是真正的AJAX。

JSONP (JSON with Padding)是一个简单高效的跨域方式,HTML中的script标签可以加载并执行其他域的javascript,于是我们可以通过script标记来动态加载其他域的资源。例如我要从域A的页面pageA加载域B的数据,那么在域B的页面pageB中我以JavaScript的形式声明pageA需要的数据,然后在 pageA中用script标签把pageB加载进来,那么pageB中的脚本就会得以执行。JSONP在此基础上加入了回调函数,pageB加载完之后会执行pageA中定义的函数,所需要的数据会以参数的形式传递给该函数。JSONP易于实现,但是也会存在一些安全隐患,如果第三方的脚本随意地执行,那么它就可以篡改页面内容,截获敏感数据。但是在受信任的双方传递数据,JSONP是非常合适的选择。

AJAX是不跨域的,而JSONP是一个是跨域的,还有就是二者接收参数形式不一样!

9.防抖和节流

1⃣️.如果用户持续点击一个按钮,如何只提交一次请求,且不影响后续使用?(其实就是如何节流这个真的问的好多!!!!)

何为节流 触发函数事件后,短时间间隔内无法连续调用,只有上一次函数执行后,过了规定的时间间隔,才能进行下一次的函数调用,一般用于http请求。

解决原理 对处理函数进行延时操作,若设定的延时到来之前,再次触发事件,则清除上一次的延时操作定时器,重新定时。

    function conso(){
          console.log('is run');
      }
      var btnUse=true;
     $("#btn").click(function(){
         if(btnUse){
             conso();
             btnUse=false;
         }
         setTimeout(function(){
             btnUse=true;
         },1500) //点击后相隔多长时间可执行
     })
复制代码

2⃣️.如何防抖?(一般都和节流一起问,一定要搞懂!!)

何为防抖 多次触发事件后,事件处理函数只执行一次,并且是在触发操作结束时执行,一般用于scroll事件。

解决原理 对处理函数进行延时操作,若设定的延时到来之前再次触发事件,则清除上一次的延时操作定时器,重新定时。

let timer;
window.onscroll  = function () {
    if(timer){
        clearTimeout(timer)
    }
    timer = setTimeout(function () {
        //滚动条位置
        let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
        console.log('滚动条位置:' + scrollTop);
        timer = undefined;
    },200)
}
复制代码

或者是这样:

function debounce(fn, wait) {
    var timeout = null;
    return function() {  
        if(timeout !== null)   clearTimeout(timeout);        
        timeout = setTimeout(fn, wait);    
    }
}
// 处理函数
function handle() {    
    console.log(Math.random()); 
}
// 滚动事件
window.addEventListener('scroll', debounce(handle, 1000));

10.数组去重的方法

1⃣️:利用ES6 Set去重(ES6中最常用)

function unique (arr) {
  return Array.from(new Set(arr))
}

2⃣️:利用for嵌套for,然后splice去重(ES5中最常用)

function unique(arr){            
        for(var i=0; i<arr.length; i++){
            for(var j=i+1; j<arr.length; j++){
                if(arr[i]==arr[j]){         //第一个等同于第二个,splice方法删除第二个
                    arr.splice(j,1);
                    j--;
                }
            }
        }
        return arr;
}

3⃣️:利用对象的属性不能相同的特点进行去重(这种数组去重的方法有问题,不建议用,但是有人考)

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var arrry= [];
     var  obj = {};
    for (var i = 0; i < arr.length; i++) {
        if (!obj[arr[i]]) {
            arrry.push(arr[i])
            obj[arr[i]] = 1
        } else {
            obj[arr[i]]++
        }
    }
    return arrry;
}

4⃣️:利用filter

function unique(arr) {
  return arr.filter(function(item, index, arr) {
    //当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
    return arr.indexOf(item, 0) === index;
  });
}

5⃣️:Map数据结构去重

function arrayNonRepeatfy(arr) {
  let map = new Map();
  let array = new Array();  // 数组用于返回结果
  for (let i = 0; i < arr.length; i++) {
    if(map .has(arr[i])) {  // 如果有该key值
      map .set(arr[i], true); 
    } else { 
      map .set(arr[i], false);   // 如果没有该key值
      array .push(arr[i]);
    }
  } 
  return array ;
}

11.数组的排序

1⃣️:冒泡排序:思路:重复遍历数组中的元素,依次比较两个相邻的元素,如果前一个元素大于后一个元素,就依靠第三个变量将它们换过来,直到所有元素遍历完。

function bubbleSort(arr){

          for(let i = 0; i < arr.length - 1; i ++){

                for(let j = 0; j < arr.length - 1 - i; j ++){

                      if(arr[j] > arr[j+1]){   

                            let tem = arr[j];

                            arr[j] = arr[j+1];

                            arr[j+1] = tem;

                      }

                }

          }

2⃣️:选择排序:思路:每一次从数组中选出最小的一个元素,存放在数组的起始位置,然后,再从剩余未排序的数组中继续寻找最小元素,然后放到已排序序列的末尾。直到全部数据元素排完。

function selectSort(arr){

          let min = 0; // 用来保存数组中最小的数的索引值

          for(let i = 0; i < arr.length - 1; i ++){

                min = i;

                for(let j = i + 1; j < arr.length; j ++){

                      if(arr[j] < arr[min]){

                            min = j;

                      }

                }

              if(min != i){

                      swap(arr,i,min);

                }

          }

          console.log(arr);

    };

    function swap(arr,index1,index2){ 

    let tem = arr[index1];

          arr[index1] = arr[index2];

          arr[index2] = tem;

    }

    selectSort([7,5,1,2,6,4,8,3,2]);    // output: [1, 2, 2, 3, 4, 5, 6, 7, 8]

3⃣️:快速排序: 对冒泡排序的一种改进。通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都比另一部分的所有数据小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

思路:
(1)找基准(一般以中间项为基准)
(2)遍历数组,小于基准的放在 left,大于基准的放在 right
(3)递归

function quickSort(arr){

    if(arr.length<=1){return arr;}    //如果数组<=1,则直接返回

    let pivotIndex = Math.floor(arr.length/2);   

    let pivot = arr.splice(pivotIndex,1)[0]; //找基准,并把基准从原数组删除

    let left=[], right=[];    //定义左右数组

    for(let i=0; i<arr.length; i++){        //比基准小的放在left,比基准大的放在right

        if(arr[i] <= pivot){

            left.push(arr[i]);   

        } else { 

        right.push(arr[i]);

        }

    }

    return quickSort(left).concat([pivot],quickSort(right));    //递归

}

12.继承

第一种,prototype的方式:

//父类 
function person(){ 
  this.hair = 'black'; 
  this.eye = 'black'; 
  this.skin = 'yellow'; 
  this.view = function(){ 
    return this.hair + ',' + this.eye + ',' + this.skin; 
  } 
} 
 
//子类 
function man(){ 
  this.feature = ['beard','strong']; 
} 
 
man.prototype = new person(); 
var one = new man(); 

这种方式最为简单,只需要让子类的prototype属性值赋值为被继承的一个实例就行了,之后就可以直接使用被继承类的方法了。

prototype 属性是啥意思呢? prototype 即为原型,每一个对象 ( 由 function 定义出来 ) 都有一个默认的原型属性,该属性是个对象类型。

并且该默认属性用来实现链的向上攀查。意思就是说,如果某个对象的属性不存在,那么将通过prototype属性所属对象来查找这个属性。如果 prototype 查找不到呢?

js会自动地找prototype的prototype属性所属对象来查找,这样就通过prototype一直往上索引攀查,直到查找到了该属性或者prototype最后为空 (“undefined”);

例如上例中的one.view()方法,js会先在one实例中查找是否有view()方法,因为没有,所以查找man.prototype属性,而prototype的值为person的一个实例,

该实例有view()方法,于是调用成功。

第二种,apply的方式:

//父类 
function person(){ 
  this.hair = 'black'; 
  this.eye = 'black'; 
  this.skin = 'yellow'; 
  this.view = function(){ 
    return this.hair + ',' + this.eye + ',' + this.skin; 
  } 
} 
 
//子类 
function man(){ 
  // person.apply(this,new Array()); 
  person.apply(this,[]); 
  this.feature = ['beard','strong']; 
} 

第三种,call+prototype的方式:

/父类 
function person(){ 
  this.hair = 'black'; 
  this.eye = 'black'; 
  this.skin = 'yellow'; 
  this.view = function(){ 
    return this.hair + ',' + this.eye + ',' + this.skin; 
  } 
} 
 
//子类 
function man(){ 
  // person.apply(this,new Array()); 
  person.call(this,[]); 
  this.feature = ['beard','strong']; 
} 
 
man.prototype = new person(); 
var one = new man(); 

13. for of , for in 和 forEach,map 的区别。

for...of循环:具有 iterator 接口,就可以用for...of循环遍历它的成员(属性值)。for...of循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象、Generator 对象,以及字符串。for...of循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。对于普通的对象,for...of结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。可以中断循环。

for...in循环:遍历对象自身的和继承的可枚举的

属性

, 不能直接获取属性值。可以中断循环。

forEach: 只能遍历数组,不能中断,没有返回值(或认为返回值是undefined)。

map: 只能遍历数组,不能中断,返回值是修改后的数组。

14.说下ES6中的class

ES6 class 内部所有定义的方法都是不可枚举的;

ES6 class 必须使用 new 调用;

ES6 class 不存在变量提升;

ES6 class 默认即是严格模式;

ES6 class 子类必须在父类的构造函数中调用super(),这样才有this对象;ES5中类继承的关系是相反的,先有子类的this,然后用父类的方法应用在this上。

15.在JS中什么是变量提升?什么是暂时性死区?

变量提升就是变量在声明之前就可以使用,值为undefined。

在代码块内,使用 let/const 命令声明变量之前,该变量都是不可用的(会抛出错误)。这在语法上,称为“暂时性死区”。暂时性死区也意味着 typeof 不再是一个百分百安全的操作。

暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

16.什么是闭包?闭包的作用是什么?闭包有哪些使用场景?

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

闭包的作用有:

  1. 封装私有变量
  2. 模仿块级作用域(ES5中没有块级作用域)
  3. 实现JS的模块

17.call、apply有什么区别?call,aplly和bind的内部是如何实现的?

call 和 apply 的功能相同,区别在于传参的方式不一样:,apply的实现和call很类似,但是需要注意他们的参数是不一样的,apply的第二个参数是数组或类数组.

bind 和 call/apply 有一个很重要的区别,一个函数被 call/apply 的时候,会直接调用,但是 bind 会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

18:new的原理是什么?通过new的方式创建对象和通过字面量创建有什么区别?

  1. 创建一个新对象。
  2. 这个新对象会被执行[[原型]]连接。
  3. 将构造函数的作用域赋值给新对象,即this指向这个新对象.
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

19。ES6新的特性有哪些?

新增了块级作用域(let,const)

提供了定义类的语法糖(class)

新增了一种基本数据类型(Symbol)

新增了变量的解构赋值

函数参数允许设置默认值,引入了rest参数,新增了箭头函数

数组新增了一些API,如 isArray / from / of 方法;数组实例新增了 entries(),keys() 和 values() 等方法

对象和数组新增了扩展运算符

ES6 新增了模块化(import/export)

ES6 新增了 Set 和 Map 数据结构

ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例

ES6 新增了生成器(Generator)和遍历器(Iterator)

20。setTimeout倒计时为什么会出现误差?

setTimeout() 只是将事件插入了“任务队列”,必须等当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码消耗时间很长,也有可能要等很久,所以并没办法保证回调函数一定会在 setTimeout() 指定的时间执行。所以, setTimeout() 的第二个参数表示的是最少时间,并非是确切时间。

HTML5标准规定了 setTimeout() 的第二个参数的最小值不得小于4毫秒,如果低于这个值,则默认是4毫秒。在此之前。老版本的浏览器都将最短时间设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常是间隔16毫秒执行。这时使用 requestAnimationFrame() 的效果要好于 setTimeout();

21.为什么 0.1 + 0.2 != 0.3 ?

0.1 + 0.2 != 0.3 是因为在进制转换和进阶运算的过程中出现精度损失。

下面是详细解释:

JavaScript使用 Number 类型表示数字(整数和浮点数),使用64位表示一个数字。

[图片上传失败...(image-bc1b91-1574390988740)]

图片说明:

计算机无法直接对十进制的数字进行运算, 需要先对照 IEEE 754 规范转换成二进制,然后对阶运算。

1.进制转换

0.1和0.2转换成二进制后会无限循环

0.1 -> 0.0001100110011001...(无限循环)
0.2 -> 0.0011001100110011...(无限循环)
复制代码复制代码

但是由于IEEE 754尾数位数限制,需要将后面多余的位截掉,这样在进制之间的转换中精度已经损失。

2.对阶运算

由于指数位数不相同,运算时需要对阶运算 这部分也可能产生精度损失。

按照上面两步运算(包括两步的精度损失),最后的结果是

0.0100110011001100110011001100110011001100110011001100

结果转换成十进制之后就是 0.30000000000000004。

22.Promise和setTimeout的区别 ?

Promise 是微任务,setTimeout 是宏任务,同一个事件循环中,promise.then总是先于 setTimeout 执行。

23.如何实现 Promise.all ?

要实现 Promise.all,首先我们需要知道 Promise.all 的功能:

  1. 如果传入的参数是一个空的可迭代对象,那么此promise对象回调完成(resolve),只有此情况,是同步执行的,其它都是异步返回的。
  2. 如果传入的参数不包含任何 promise,则返回一个异步完成. promises 中所有的promise都“完成”时或参数中不包含 promise 时回调完成。
  3. 如果参数中有一个promise失败,那么Promise.all返回的promise对象失败
  4. 在任何情况下,Promise.all 返回的 promise 的完成状态的结果都是一个数组
Promise.all = function (promises) {
    return new Promise((resolve, reject) => {
        let index = 0;
        let result = [];
        if (promises.length === 0) {
            resolve(result);
        } else {
            function processValue(i, data) {
                result[i] = data;
                if (++index === promises.length) {
                    resolve(result);
                }
            }
            for (let i = 0; i < promises.length; i++) {
                //promises[i] 可能是普通值
                Promise.resolve(promises[i]).then((data) => {
                    processValue(i, data);
                }, (err) => {
                    reject(err);
                    return;
                });
            }
        }
    });
}

24.如何实现 Promise.finally ?

不管成功还是失败,都会走到finally中,并且finally之后,还可以继续then。并且会将值原封不动的传递给后面的then.

Promise.prototype.finally = function (callback) {
    return this.then((value) => {
        return Promise.resolve(callback()).then(() => {
            return value;
        });
    }, (err) => {
        return Promise.resolve(callback()).then(() => {
            throw err;
        });
    });
}

25.什么是函数柯里化?实现 sum(1)(2)(3) 返回结果是1,2,3之和

函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

function sum(a) {
    return function(b) {
        return function(c) {
            return a+b+c;
        }
    }
}
console.log(sum(1)(2)(3)); // 6

26.谈谈对 async/await 的理解,async/await 的实现原理是什么?

async/await 就是 Generator 的语法糖,使得异步操作变得更加方便。来张图对比一下:

[图片上传失败...(image-fc5dc6-1574390988740)]

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 函数和自动执行器,包装在一个函数里。

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);
});

27.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() 运行在后台标签页或者隐藏的 <iframe> 里时,requestAnimationFrame() 会被暂停调用以提升性能和电池寿命(大多数浏览器中)。

28.简述下对 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"); //向主进程发送消息
};复制代码

仅是最简示例代码,项目中通常是将一些耗时较长的代码,放在子线程中运行。

29.跨域的方法有哪些?原理是什么?

知其然知其所以然,在说跨域方法之前,我们先了解下什么叫跨域,浏览器有同源策略,只有当“协议”、“域名”、“端口号”都相同时,才能称之为是同源,其中有一个不同,即是跨域。

那么同源策略的作用是什么呢?同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

那么我们又为什么需要跨域呢?一是前端和服务器分开部署,接口请求需要跨域,二是我们可能会加载其它网站的页面作为iframe内嵌。

跨域的方法有哪些?

常用的跨域方法

  1. jsonp

尽管浏览器有同源策略,但是 <script> 标签的 src 属性不会被同源策略所约束,可以获取任意服务器上的脚本并执行。jsonp 通过插入script标签的方式来实现跨域,参数只能通过url传入,仅能支持get请求。

实现原理:

Step1: 创建 callback 方法

Step2: 插入 script 标签

Step3: 后台接受到请求,解析前端传过去的 callback 方法,返回该方法的调用,并且数据作为参数传入该方法

Step4: 前端执行服务端返回的方法调用

下面代码仅为说明 jsonp 原理,项目中请使用成熟的库。分别看一下前端和服务端的简单实现:

//前端代码
function jsonp({url, params, cb}) {
    return new Promise((resolve, reject) => {
        //创建script标签
        let script = document.createElement('script');
        //将回调函数挂在 window 上
        window[cb] = function(data) {
            resolve(data);
            //代码执行后,删除插入的script标签
            document.body.removeChild(script);
        }
        //回调函数加在请求地址上
        params = {...params, cb} //wb=b&cb=show
        let arrs = [];
        for(let key in params) {
            arrs.push(`${key}=${params[key]}`);
        }
        script.src = `${url}?${arrs.join('&')}`;
        document.body.appendChild(script);
    });
}
//使用
function sayHi(data) {
    console.log(data);
}
jsonp({
    url: 'http://localhost:3000/say',
    params: {
        //code
    },
    cb: 'sayHi'
}).then(data => {
    console.log(data);
});复制代码
//express启动一个后台服务
let express = require('express');
let app = express();

app.get('/say', (req, res) => {
    let {cb} = req.query; //获取传来的callback函数名,cb是key
    res.send(`${cb}('Hello!')`);
});
app.listen(3000);复制代码

从今天起,jsonp的原理就要了然于心啦~

  1. cors

jsonp 只能支持 get 请求,cors 可以支持多种请求。cors 并不需要前端做什么工作。

简单跨域请求:

只要服务器设置的Access-Control-Allow-Origin Header和请求来源匹配,浏览器就允许跨域

  1. 请求的方法是get,head或者post。
  2. Content-Type是application/x-www-form-urlencoded, multipart/form-data 或 text/plain中的一个值,或者不设置也可以,一般默认就是application/x-www-form-urlencoded。
  3. 请求中没有自定义的HTTP头部,如x-token。(应该是这几种头部 Accept,Accept-Language,Content-Language,Last-Event-ID,Content-Type)
//简单跨域请求
app.use((req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', 'XXXX');
});复制代码

带预检(Preflighted)的跨域请求

不满于简单跨域请求的,即是带预检的跨域请求。服务端需要设置 Access-Control-Allow-Origin (允许跨域资源请求的域) 、 Access-Control-Allow-Methods (允许的请求方法) 和 Access-Control-Allow-Headers (允许的请求头)

app.use((req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', 'XXX');
    res.setHeader('Access-Control-Allow-Headers', 'XXX'); //允许返回的头
    res.setHeader('Access-Control-Allow-Methods', 'XXX');//允许使用put方法请求接口
    res.setHeader('Access-Control-Max-Age', 6); //预检的存活时间
    if(req.method === "OPTIONS") {
        res.end(); //如果method是OPTIONS,不做处理
    }
});复制代码

更多CORS的知识可以访问: HTTP访问控制(CORS)

  1. nginx 反向代理

使用nginx反向代理实现跨域,只需要修改nginx的配置即可解决跨域问题。

A网站向B网站请求某个接口时,向B网站发送一个请求,nginx根据配置文件接收这个请求,代替A网站向B网站来请求。 nginx拿到这个资源后再返回给A网站,以此来解决了跨域问题。

例如nginx的端口号为 8090,需要请求的服务器端口号为 3000。(localhost:8090 请求 localhost:3000/say)

nginx配置如下:

server {
    listen       8090;

    server_name  localhost;

    location / {
        root   /Users/liuyan35/Test/Study/CORS/1-jsonp;
        index  index.html index.htm;
    }
    location /say {
        rewrite  ^/say/(.*)$ /$1 break;
        proxy_pass   http://localhost:3000;
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Credentials' 'true';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
    }
  ## others
}复制代码
  1. websocket

Websocket 是 HTML5 的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。

Websocket 不受同源策略影响,只要服务器端支持,无需任何配置就支持跨域。

前端页面在 8080 的端口。

let socket = new WebSocket('ws://localhost:3000'); //协议是ws
socket.onopen = function() {
    socket.send('Hi,你好');
}
socket.onmessage = function(e) {
    console.log(e.data)
}复制代码

服务端 3000端口。可以看出websocket无需做跨域配置。

let WebSocket = require('ws');
let wss = new WebSocket.Server({port: 3000});
wss.on('connection', function(ws) {
    ws.on('message', function(data) {
        console.log(data); //接受到页面发来的消息'Hi,你好'
        ws.send('Hi'); //向页面发送消息
    });
});复制代码
  1. postMessage

postMessage 通过用作前端页面之前的跨域,如父页面与iframe页面的跨域。window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。

话说工作中两个页面之前需要通信的情况并不多,我本人工作中,仅使用过两次,一次是H5页面中发送postMessage信息,ReactNative的webview中接收此此消息,并作出相应处理。另一次是可轮播的页面,某个轮播页使用的是iframe页面,为了解决滑动的事件冲突,iframe页面中去监听手势,发送消息告诉父页面是否左滑和右滑。

子页面向父页面发消息

父页面

window.addEventListener('message', (e) => {
    this.props.movePage(e.data);
}, false);复制代码

子页面(iframe):

if(/*左滑*/) {
    window.parent && window.parent.postMessage(-1, '*')
}else if(/*右滑*/){
    window.parent && window.parent.postMessage(1, '*')
}复制代码

父页面向子页面发消息

父页面:

let iframe = document.querySelector('#iframe');
iframe.onload = function() {
    iframe.contentWindow.postMessage('hello', 'http://localhost:3002');
}复制代码

子页面:

window.addEventListener('message', function(e) {
    console.log(e.data);
    e.source.postMessage('Hi', e.origin); //回消息
});复制代码
  1. node 中间件

node 中间件的跨域原理和nginx代理跨域,同源策略是浏览器的限制,服务端没有同源策略。

node中间件实现跨域的原理如下:

1.接受客户端请求

2.将请求 转发给服务器。

3.拿到服务器 响应 数据。

4.将 响应 转发给客户端。

不常用跨域方法

以下三种跨域方式很少用,如有兴趣,可自行查阅相关资料。

  1. window.name + iframe
  2. location.hash + iframe
  3. document.domain (主域需相同)

30 js异步加载的方式有哪些?

  1. <script> 的 defer 属性,HTML4 中新增
  2. <script> 的 async 属性,HTML5 中新增

<script>标签打开defer属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。

defer 和 async 的区别在于: defer要等到整个页面在内存中正常渲染结束,才会执行;

async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。defer是“渲染完再执行”,async是“下载完就执行”。

如果有多个 defer 脚本,会按照它们在页面出现的顺序加载。

多个async脚本是不能保证加载顺序的。

31.实现双向绑定 Proxy 与 Object.defineProperty 相比优劣如何?

  1. Object.definedProperty 的作用是劫持一个对象的属性,劫持属性的getter和setter方法,在对象的属性发生变化时进行特定的操作。而 Proxy 劫持的是整个对象。
  2. Proxy 会返回一个代理对象,我们只需要操作新对象即可,而 Object.defineProperty 只能遍历对象属性直接修改。
  3. Object.definedProperty 不支持数组,更准确的说是不支持数组的各种API,因为如果仅仅考虑arry[i] = value 这种情况,是可以劫持的,但是这种劫持意义不大。而 Proxy 可以支持数组的各种API。
  4. 尽管 Object.defineProperty 有诸多缺陷,但是其兼容性要好于 Proxy.

PS: Vue2.x 使用 Object.defineProperty 实现数据双向绑定,V3.0 则使用了 Proxy.

//拦截器
let obj = {};
let temp = 'Yvette';
Object.defineProperty(obj, 'name', {
    get() {
        console.log("读取成功");
        return temp
    },
    set(value) {
        console.log("设置成功");
        temp = value;
    }
});

obj.name = 'Chris';
console.log(obj.name);复制代码

PS: Object.defineProperty 定义出来的属性,默认是不可枚举,不可更改,不可配置【无法delete】

我们可以看到 Proxy 会劫持整个对象,读取对象中的属性或者是修改属性值,那么就会被劫持。但是有点需要注意,复杂数据类型,监控的是引用地址,而不是值,如果引用地址没有改变,那么不会触发set。

let obj = {name: 'Yvette', hobbits: ['travel', 'reading'], info: {
    age: 20,
    job: 'engineer'
}};
let p = new Proxy(obj, {
    get(target, key) { //第三个参数是 proxy, 一般不使用
        console.log('读取成功');
        return Reflect.get(target, key);
    },
    set(target, key, value) {
        if(key === 'length') return true; //如果是数组长度的变化,返回。
        console.log('设置成功');
        return Reflect.set([target, key, value]);
    }
});
p.name = 20; //设置成功
p.age = 20; //设置成功; 不需要事先定义此属性
p.hobbits.push('photography'); //读取成功;注意不会触发设置成功
p.info.age = 18; //读取成功;不会触发设置成功复制代码

最后,我们再看下对于数组的劫持,Object.definedProperty 和 Proxy 的差别

Object.definedProperty 可以将数组的索引作为属性进行劫持,但是仅支持直接对 arry[i] 进行操作,不支持数组的API,非常鸡肋。

let arry = []
Object.defineProperty(arry, '0', {
    get() {
        console.log("读取成功");
        return temp
    },
    set(value) {
        console.log("设置成功");
        temp = value;
    }
});

arry[0] = 10; //触发设置成功
arry.push(10); //不能被劫持复制代码

Proxy 可以监听到数组的变化,支持各种API。注意数组的变化触发get和set可能不止一次,如有需要,自行根据key值决定是否要进行处理。

let hobbits = ['travel', 'reading'];
let p = new Proxy(hobbits, {
    get(target, key) {
        // if(key === 'length') return true; //如果是数组长度的变化,返回。
        console.log('读取成功');
        return Reflect.get(target, key);
    },
    set(target, key, value) {
        // if(key === 'length') return true; //如果是数组长度的变化,返回。
        console.log('设置成功');
        return Reflect.set([target, key, value]);
    }
});
p.splice(0,1) //触发get和set,可以被劫持
p.push('photography');//触发get和set
p.slice(1); //触发get;因为 slice 是不会修改原数组的

32.Object.is() 与比较操作符 ===、== 有什么区别?

以下情况,Object.is认为是相等

两个值都是 undefined
两个值都是 null
两个值都是 true 或者都是 false
两个值是由相同个数的字符按照相同的顺序组成的字符串
两个值指向同一个对象
两个值都是数字并且
都是正零 +0
都是负零 -0
都是 NaN
都是除零和 NaN 外的其它同一个数字复制代码

Object.is() 类似于 ===,但是有一些细微差别,如下:

  1. NaN 和 NaN 相等
  2. -0 和 +0 不相等
console.log(Object.is(NaN, NaN));//true
console.log(NaN === NaN);//false
console.log(Object.is(-0, +0)); //false
console.log(-0 === +0); //true复制代码

Object.is 和 ==差得远了, == 在类型不同时,需要进行类型转换

33.toStringString的区别

  1. toString()可以将数据都转为字符串,但是nullundefined不可以转换。

    console.log(null.toString())
    //报错 TypeError: Cannot read property 'toString' of null
    
    console.log(undefined.toString())
    //报错 TypeError: Cannot read property 'toString' of undefined
    复制代码
    
  2. toString()括号中可以写数字,代表进制

    二进制:.toString(2);

    八进制:.toString(8);

    十进制:.toString(10);

    十六进制:.toString(16);

  1. String()可以将nullundefined转换为字符串,但是没法转进制字符串

    console.log(String(null));
    // null
    console.log(String(undefined));
    // undefined
    

34.TCP的三次握手和四次挥手

三次握手

四次挥手

35.为什么建立连接是三次握手,而断开连接是四次挥手呢?

建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。 而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。

36.map和forEach的区别

相同点

不同点

注意:forEach对于空数组是不会调用回调函数的。

37.setTimeout 和 setInterval的机制

因为js是单线程的。浏览器遇到etTimeout 和 setInterval会先执行完当前的代码块,在此之前会把定时器推入浏览器的待执行时间队列里面,等到浏览器执行完当前代码之后会看下事件队列里有没有任务,有的话才执行定时器里的代码

38.JS中常见的异步任务

定时器、ajax、事件绑定、回调函数、async await、promise

39.浏览器渲染的主要流程是什么?

将html代码按照深度优先遍历来生成DOM树。 css文件下载完后也会进行渲染,生成相应的CSSOM。 当所有的css文件下载完且所有的CSSOM构建结束后,就会和DOM一起生成Render Tree。 接下来,浏览器就会进入Layout环节,将所有的节点位置计算出来。 最后,通过Painting环节将所有的节点内容呈现到屏幕上。

40.ajax中get和post请求的区别

get 一般用于获取数据

get请求如果需要传递参数,那么会默认将参数拼接到url的后面;然后发送给服务器;

get请求传递参数大小是有限制的;是浏览器的地址栏有大小限制;

get安全性较低

get 一般会走缓存,为了防止走缓存,给url后面每次拼的参数不同;放在?后面,一般用个时间戳

post 一般用于发送数据

post传递参数,需要把参数放进请求体中,发送给服务器;

post请求参数放进了请求体中,对大小没有要求;

post安全性比较高;

post请求不会走缓存;

42.DOM diff原理

43.动手实现一个bind(原理通过apply,call)

一句话概括:1.bind()返回一个新函数,并不会立即执行。
        2.bind的第一个参数将作为他运行时的this,之后的一系列参数将会在传递的实参前传入作为他的参数
        3.bind返回函数作为构造函数,就是可以new的,bind时指定的this值就会消失,但传入的参数依然生效
Function.prototype.bind = function (obj, arg) {
   var arg = Array.prototype.slice.call(arguments, 1);
   var context = this;
   var bound = function (newArg) {
   arg = arg.concat(Array.prototype.slice.call(newArg);
   return context.apply(obj, arg)
}
  var F =  function () {}  // 在new一个bind会生成新函数,必须的条件就是要继承原函数的原型,因此用到寄生继承来完成我们的过程
  F.prototype = context.prototype;
  bound.prototype =  new F();
  return bound;
}   
上一篇下一篇

猜你喜欢

热点阅读