面试总结、归纳

瘦欲@ 提交于 2019-11-30 04:22:59

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,但是对于正则表达式,日期,副本这些引用数据类型,它会全部识别为对象; instanceof同样也是一个运算符,它就能很好地识别数据具体是哪一种引用类型。它与isPrototypeOf的区别就是它是检测构造函数的原型是否存在于指定对象的原型链当中;而isPrototypeOf是用于检测调用此方法的对象是否存在于指定对象的原型链中,因此本质上就是检测目标不同。

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

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

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

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

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

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

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

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在更严格的条件下运行。

好处:

消除Javascript语法的一些不合理,不严谨之处,减少一些怪异行为;
消除代码运行的一些不安全分开,保证代码运行的安全;
提高编译器效率,增加运行速度;
为未来新版本的Javascript准备铺垫。
坏处:

同样的代码,在“严格模式”中,可能会有不一样的运行结果;
一些在“正常模式”下可以运行的语句,在“严格模式”下将不能运行。

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

JSONP(带填充的JSON)是一个简单高效的跨域方式,HTML中的脚本标签可以加载并执行其他域的javascript,于是我们可以通过脚本标记来动态加载其他域的资源。例如我要从域A的页面pageA加载域B的数据,那么在域B的页面pageB中我以JavaScript的形式声明pageA需要的数据,然后在pageA中用脚本标签把pageB加载进来,那么pageB中的脚本就会实现执行。JSONP在此基础上加入了有关函数,pageB加载完成之后会执行页面A中定义的函数,所需要的数据会以参数的形式传递给该函数。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.如何防抖?(一般都和节流一起问,一定要搞懂!!)

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

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

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.大量去重的方法

利用ES6 Set去重(ES6中最常用)

function unique (arr) {
  return Array.from(new Set(arr))
}
利用for嵌套,然后拼接去重(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;
}

利用过滤器

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

地图数据结构去重

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;

                      }

                }

          }

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

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]

快速排序:对冒泡排序的一种改进。通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都比另一部分的所有数据小,然后再按此方法对两个部分数据分别进行快速排序,整个排序过程可以递归进行,从而达到整个数据变成有序序列。
思路:(1)找基准(一般以中间项为基准)(2)遍历数组,小于基准的放在左,大于基准的放在右(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为为原型,每一个对象(由函数定义出来)都有一个原型的原型属性,该属性是一个对象类型。

就是说,如果某个对象的属性不存在,那么将通过原型属性所属对象来查找这个属性。如果原型不到呢呢?

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

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

该实例有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,for和和forEach,map的区别。

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

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

属性

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

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

map:只能遍历时间表,不能中断,返回值是修改后的片断。

14.说下ES6中的课程

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

ES6类必须使用new调用;

ES6类不存在变量提升;

ES6类默认即是严格模式;

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

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

变量提升就是变量在声明之前就可以使用,变量未定义。

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

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

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

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

闭包的作用有:

封装专有变量
模仿块级作用域(ES5中没有块级作用域)
实现JS的模块

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

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

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

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

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

19。ES6新的特性有哪些?

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

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

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

新增了变量的解构赋值

函数参数允许设置默认值,更新参数

数组添加了一些API,如isArray / from / of of method;复制实例添加了entry(),keys()和values()等方法

对象和合并添加了扩展运算符

ES6新增了预设(导入/导出)

ES6添加了Set和Map数据结构

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

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

20。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位表示一个数字。

img

图片说明:

第0位:符号位,0表示正数,1表示负数
第1位到第11位:存储指数部分(e)
第12位到第63位:存储小数部分(即有效数字)f
计算机无法直接对十进制的数字进行运算,需要先对照IEEE 754规范转换成二进制,然后对阶运算。

1.二进制转换

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

0.1 -> 0.0001100110011001…(无限循环)
0.2 -> 0.0011001100110011…(无限循环)
复制代码复制代码
但是由于IEEE 754尾数位数限制,需要将多余的位截掉,这样在二进制之间的转换中精度已经损失。

2.对阶运算

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

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

0.01001100110011001100110011001100110011001100110011001100

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

22.Promise和setTimeout的区别?

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

23.如何实现Promise.all?

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

如果预设的参数是一个空的可重复对象,那么此承诺对象将重新完成(解决),只有此情况,是同步执行的,其他都是异步返回的。
如果预设的参数不包含任何诺言,则返回一个初始完成。诺言中所有的promise都“完成”时或参数中不包含诺言时完成。
如果参数中有一个promise失败,那么Promise.all返回的promise对象失败
在任何情况下,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之后,还可以继续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的语法糖,使得异步操作变得更加方便。来张图对比一下:

img

async函数就是将Generator函数的星号(*)替换成async,将产生替换成await。

我们说async是Generator的语法糖,那么这个糖究竟甜在哪呢呢?

1)async函数内置执行器,函数调用之后,会自动执行,输出最后结果。而发电机需要调用下一个或配合co模块使用。

2)更好的语义,async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

3)更广的适用性。co模块约定,yield命令后面只能是Thunk函数或Promise对象,而async函数的await命令后面,可以是Promise对象和原始类型的值。

4)返回值是Promise,异步函数的返回值是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()在后台标签页或隐藏的里时,requestAnimationFrame()会被暂停调用以提升性能和电池寿命(大多数浏览器中)。

28.简述下对webWorker的理解?

HTML5则提出了Web Worker标准,表示js支持多线程,但是子线程完全受主线程控制和不能操作dom,只有主线程可以操作dom,所以js本质上依然是单线程语言。

网络工作者就是在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内嵌。

跨域的方法有什么?

常用的跨域方法

jsonp
尽管浏览器有相关策略,但是

实现原理:

步骤1:建立回呼方法

第二步:插入脚本标签

第3步:后台接受到请求,解析前端传过去的回调方法,返回该方法的调用,以及数据作为参数替换该方法

步骤4:预先执行服务端返回的方法调用

下面的代码仅说明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的原理就要了然于心啦〜

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

简单跨域请求:

只要服务器设置的Access-Control-Allow-Origin标头和请求来源匹配,浏览器就可以跨域

请求的方法是get,head还是post。
Content-Type是application / x-www-form-urlencoded,multipart / form-data或text / plain中的一个值,或者不设置也可以,一般来说就是application / x-www-form-urlencoded。
(应该是这几句话接受,接受语言,内容语言,最后事件ID,内容类型)
//简单跨域请求

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)

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
}

网络套接字
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'); //向页面发送消息
    });
});

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); //回消息
});

节点中间件
node中间件的跨域原理和nginx代理跨域,关联策略是浏览器的限制,服务端没有类似策略。

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

1.接受客户端请求

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

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

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

不常用跨域方法

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

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

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

<script> 的defer属性,HTML4中新增
<script> 的async属性,HTML5中新增
<script>标签打开Defer属性,脚本就会初始化加载。渲染引擎遇到此行命令,就会开始下载外部脚本,但不会等它下载并执行,或者直接执行后面的命令。

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

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

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

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

# 31.实现双向绑定Proxy与Object.defineProperty引用优劣如何?
Object.definedProperty的作用是劫持一个对象的属性,劫持属性的getter和setter方法,在对象的属性发生变化时进行特定的操作。而代理劫持的是整个对象。
代理会返回一个代理对象,我们只需要操作新对象即可,而Object.defineProperty 只能遍历对象属性直接修改。
Object.definedProperty不支持分散,更准确的说是不支持数组的各种API,因为如果仅仅考虑arry [i] = value这种情况,是可以劫持的,但是这种劫持意义不大。而Proxy可以支持数组的各种API。
尽管Object.defineProperty有很多缺陷,但是其兼容性要好于代理。
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定义出来的属性,替代是不可枚举,不可更改,不可配置【无法删除】

我们可以看到代理会劫持整个对象,读取对象中的属性或者是修改属性值,那么就会被劫持。但是有点需要注意,复杂数据类型,监控的是引用地址,而不是值,如果引用地址没有改变,那么不会触发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); //不能被劫持复制代码

代理可以侦听到层次的变化,支持各种API。注意分区的变化触发获取和设置可能不止一次,如有需要,自行根据键值决定是否要进行处理。

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()可能===,但是有一些细微差异,如下:

NaN和NaN近似
-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. toString和String的区别

toString
toString()可以将数据都转为同轴,但是null和undefined不可以转换。

console.log(null.toString())
//报错 TypeError: Cannot read property ‘toString’ of null

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

二进制:.toString(2);

八进制:.toString(8);

十进制:.toString(10);

十六进制:.toString(16);

String
String()可以将null和undefined转换为字符串,但是没法转二进制字符串

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

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

三次握手

第一次握手:客户端发送一个SYN码给服务器,要求建立数据连接;
第二次握手:服务器SYN和自己处理一个SYN(标志);叫SYN + ACK(确认包);发送给客户端,可以建立连接
第三次握手:客户端再次发送ACK向服务器,服务器验证ACK没有问题,则建立起连接;
四次挥手

第一次挥手:客户端发送FIN(结束)报文,通知服务器数据已经传输完毕;
第二次挥手:服务器接收到之后,通知客户端我收到了SYN,发送ACK(确认)给客户端,数据还没有传输完成
第三次挥手:服务器已经传输完毕,再次发送FIN通知客户端,数据已经传输完毕
第四次挥手:客户端再次发送ACK,进入TIME_WAIT状态;服务器和客户端关闭连接;

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

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

36.map和forEach的区别

相同点

都是针对每个和地图方法里每次执行匿名函数都支持3个参数,参数分别是item(当前每项),index(索引值),arr(原表)的循环用那个的时候就写这个匿名函数中的这个都是指向window只能遍历数组
不同点

map()方法不会改变原始数组map()方法不; map方法返回一个新的数组,数组中的元素为原始数组调用函数处理后的值。静态空序列进行检测forEach()方法用于调用数组的每个元素,将元素传给给函数。(没有返回,返回值是undefined)
注意:forEach对于空副本是不会调用引用函数的。

37.setTimeout和setInterval的机制

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

38.JS中常见的异步任务

计时器,ajax,事件绑定,特定函数,异步等待,承诺

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

将HTML代码按照深度优先遍历历程生成DOM树。css文件下载完后也会进行渲染,生成相应的CSSOM。当所有的cs文件下载完且所有的CSSOM生成结束后,就会和DOM一起生成渲染树。 。,接下来,浏览器就会进入布局转换,将所有的二进制位置计算出来。。,最后,通过绘画将所有的例程内容呈现到屏幕上。

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

获取一般用于获取数据

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

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

得到安全性证据

得到一般会走缓存,为了防止走缓存,给URL后面每次拼写的参数不同;放在?后面,一般用个累积

发布一般用于发送数据

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!