互联网寒冬之际,各大公司都缩减了HC,甚至是采取了“裁员”措施,在这样的大环境之下,想要获得一份更好的工作,必然需要付出更多的努力。
一年前,也许你搞清楚闭包,this,原型链,就能获得认可。但是现在,很显然是不行了。本文梳理出了一些面试中有一定难度的高频原生JS问题,部分知识点可能你之前从未关注过,或者看到了,却没有仔细研究,但是它们却非常重要。

1. 基本类型有哪几种?null 是对象吗?基本数据类型和复杂数据类型存储有什么区别?

  • 1)js基本类型有6种:undefined,null,bool,number,string,symbol(ES6新增)
  • 2)虽然typeof null返回的值是object,但是null不是对象,而是基本数据类型的一种。
  • 3)基本数据类型存储在栈内存,存储的是
    复杂数据类型的值存储在堆内存,地址【指向堆中的值】存储在栈内存
    当我们把对象赋值给另外一个变量的时候,复制的是地址,它们指向同一块内存空间,当其中一个对象改变时,另一个对象也会改变。

2. typeof 是否正确判断类型? instanceof呢? instanceof 的实现原理是什么?

  • typeof 能够正确的判断基本数据类型,但是除了null(typeof null 输出的是object)。
    typeof 无法准确判断复杂数据类型(typeof 一个函数可以输出‘function’,除此之外,输出的全是object,我们无法知道对象的类型)
  • instanceof 无法正确判断基本数据类型,但是可以准确判断复杂数据类型。
    instanceof 是通过原型链判断的,A instanceof B,在A的原型链中层层查找,是否有原型等于B.prototype,如果一直找到A的原型链的顶端(null,即Object.proto.proto),仍然不等于B.prototype,那么返回false,否则返回true
    正确判断数据类型请戳:https://github.com/YvetteLau/Blog/blob/master/JS/data-type.js
    instanceof的实现代码:
    // L instanceof R
    function instance_of(L,R){ //L 为左表达式,R为右表达式
     var O = R.prototype; //取R的显示原型
     L = L.__proto__; //取L的隐式原型
     while(true){
         if(L === null) //已经找到顶层
             return false;
         if(O === L) //当O严格等于L时,返回true
             return true;
         L = L.__proto__; //继续向上一层原型链查找
     }   
    }

3. for ,for of , for in ,().each 和 forEach,map 的区别。

  • for
    Javascript中的for循环,它用来遍历数组
  • for of
    ES6中新增加的语法 for of 语句创建一个循环来迭代可迭代的对象。在 ES6 中引入的 for of 循环,以替代 for in 和 forEach() ,并支持新的迭代协议。for of 允许遍历 Arrays(数组), Strings(字符串), Maps(映射), Sets(集合)等可迭代的数据结构等
    对于普通的对象,for...of结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。
    //循环一个Map:
    let iterable = new Map([["a", 1], ["b", 2], ["c", 3]]);    
    for (let [key, value] of iterable) {
    console.log(value);
    }
    // 1 2 3
    for (let entry of iterable) {
    console.log(entry);
    }
    //[a,1] [a,2] [a,3]
    //循环一个Set: ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
    let iterable = new Set([1, 1, 2, 2, 3, 3]);    
    for (let value of iterable) {
    console.log(value);
    }
    //1 2 3
    //循环一个拥有enumerable属性的对象
    //for of循环并不能直接使用在普通的对象上,但如果我们按对象所拥有的属性进行循环,可使用内置的Object.keys()方法:
    for (var key of Object.keys(someObject)) {
    console.log(key + ": " + someObject[key]);
    }
    

原生js中的Object.keys方法详解:https://segmentfault.com/a/1190000015619348

  • for in
    遍历对象自身的和继承的可枚举的属性, 不能直接获取属性值。可以中断循环。
    for(var item in arr|obj){} 可以用于遍历数组和对象

    //遍历对象时,item表示key值,arr表示key值对应的value值 obj[item]
    var obj = {a:1,b:2,c:3};
    for(let item in obj){
    console.log('obj.' + item + '=' + obj[item]);
    }
    //遍历数组时,item表示索引值, arr表示当前索引值对应的元素 arr[item]
    var arr = ['a','b','c'];
    for (var item in arr) {
      console.log(arr[item]) //a b c
    }
  • forEach()
    只能遍历数组,不能continue跳过或者break终止循环(不能中断),没有返回值(或认为返回值是undefined)。
    forEach循环可以直接取到元素,同时也可以取到index值。

    let arr = ['a','b','c','d']
    arr.forEach(function(val,index,arr){
    //val是当前元素,index是当前元素索引,arr是数组
      console.log('index:'+ index + ',' + 'val:' + val)
    })
  • $.each(jQuery)
    $.each(arr|obj,function(key,val)){}
    可以用来遍历数组和对象,其中key表示索引值,val表示value值

    var arr = ['a','b','c']
    $.each(arr, function(key, val) {
      console.log(key, val);
    })
    //0 a  //1 b  //2 c
  • $().each()(jQuery)
    $().each()在dom处理上面用的较多,主要是用来遍历DOMList。如果页面有多个input标签类型为checkbox,对于这时用$().each()来处理多个checkbox

    $("input[name = 'checkbox']").each(function(i){
      if($(this).attr('checked') == true){
      //操作代码
      }
    })
  • forEach是否会改变原数组?
    除了forEach之外,map等API,也有同样的问题。

    //数组项是基本类型
    let array = [1, 2, 3, 4];
    array.forEach((item) => {
      item *= 10;
    });
    console.log(array); //[1, 2, 3, 4]
    array.forEach((item) => {
      array[1] = 10; //直接操作数组
    });
    console.log(array); //[ 1, 10, 3, 4 ]
    //数组项是复杂类型
    let array2 = [
      { name: "Yve" },
      { age: 20 }
    ];
    array2.forEach((item) => {
      item.name = 10;
    });
    console.log(array2);//[ { name: 10 }, { age: 20, name: 10 } ]
    

4. 如何判断一个变量是不是数组

  • 1)使用Array.isArray(arr)判断,如果返回true,则是数组
  • 2)使用instanceof Array判断,如果返回true,则是数组
  • 3)使用Object.prototype.toString.call判断,如果值是[object Array],则是数组
  • 4)使用constructor来判断,如果arr.constructor === Array,则是数组(这种判断方式不准确,因为可以指定obj.constructor = Array)
    function fn() {
      console.log(Array.isArray(arguments));   //false; 因为arguments是类数组,但不是数组
      console.log(Array.isArray([1,2,3,4]));   //true
      console.log(arguments instanceof Array); //fasle
      console.log([1,2,3,4] instanceof Array); //true
      console.log(Object.prototype.toString.call(arguments)); //[object Arguments]
      console.log(Object.prototype.toString.call([1,2,3,4])); //[object Array]
      console.log(arguments.constructor === Array); //false
      arguments.constructor = Array;
      console.log(arguments.constructor === Array); //true
      console.log(Array.isArray(arguments));        //false
    }
    fn(1,2,3,4);
    

5. 类数组与数组的区别

类数组:
1)拥有length属性,其它属性(索引)为非负整数(对象中的索引会被当做字符串来处理)。元素按序保存在对象中,可以通过索引访问
2)不具有数组所具有的方法
3)类数组是普通对象,而真实数组是Array类型
常见的类数组:函数的参数argumentsDOM对象列表(比如通过 document.querySelectorAll 得到的列表)、jQuery 对象 (比如 $("div"))

  • 类数组可以转换为数组:
    类数组转换为数组的方法:https://segmentfault.com/a/1190000015625985
    //第一种方法:借用了数组原型中的slice方法(没有传入参数时,开始和结束索引为0和arr.length),返回一个数组。
    Array.prototype.slice.call(arrayLike, start);
    //第二种方法:扩展运算符(…)也可以将某些数据结构转为数组
    [...arrayLike];
    //第三种方法:Array.from()是ES6中新增的方法,可以将两类对象转为真正的数组:类数组对象和可遍历(iterable)对象(包括ES6新增的数据结构Set和Map)
    Array.from(arrayLike);
    

6. == 和 === 有什么区别?

  • === 不需要进行类型转换,只有类型相同并且值相等时,才返回 true.
  • == 如果两者类型不同,首先需要进行类型转换。具体流程如下:
  1. 首先判断两者类型是否相同,如果相等,判断值是否相等.
  2. 如果类型不同,进行类型转换
  3. 判断比较的是否是 null 或者是 undefined, 如果是, 返回 true .
  4. 判断两者类型是否为 string 和 number, 如果是, 将字符串转换成 number
  5. 判断其中一方是否为 boolean, 如果是, 将 boolean 转为 number 再进行判断
  6. 判断其中一方是否为 object 且另一方为 string、number 或者 symbol , 如果是, 将 object 转为原始类型再进行判断

例题如下:

let person1 = {
    age : 25;
}
let person2 = person1;
person2.age = 20;
console.log(person1 === person2);
//true,复杂数据类型,比较的是引用地址

思考:[] == ![] ?? true还是false
1.首先,我们需要知道 ! 优先级是高于 == (更多运算符优先级可查看: 运算符优先级)
2. []引用类型转换成布尔值都是true,因此![]的是false
3. 根据上面的比较步骤中的第五条,其中一方是 boolean,将 boolean 转为 number 再进行判断,false转换成 number,对应的值是 0.
4. 根据上面比较步骤中的第六条,有一方是 number,那么将object也转换成Number,空数组转换成数字,对应的值是0.(空数组转换成数字,对应的值是0,如果数组中只有一个数字,转成number就是这个数字,其它情况,均为NaN)
5. 0 == 0; 为true

7. ES6中的class和ES5的类有什么区别?

https://segmentfault.com/a/1190000010654915

  1. ES6 class 内部所有定义的方法都是不可枚举的;
  2. ES6 class 必须使用 new 调用;
  3. ES6 class 不存在变量提升;
  4. ES6 class 默认即是严格模式;
  5. ES6 class 子类必须在父类的构造函数中调用super(),这样才有this对象;ES5中类继承的关系是相反的,先有子类的this,然后用父类的方法应用在this上。

8. 数组的哪些API会改变原数组?

修改原数组的API有:
splice/reverse/fill/copyWithin/sort/push/pop/unshift/shift

  • array.splice(index,count,add)
    既可以删除特定的元素,也可以在特定位置增加元素,也可以删除增加同时搞定,index是起始位置,hm是要删除元素的个数,add是要增加的元素
  • array.reverse()
    把数组反向排序
  • array.fill(给定值,填充起始位置,填充截至位置)
    用给定值填充数组
  • array.copyWithin(target, start = 0, end = this.length)
    将指定位置的成员复制到其它位置(会覆盖原有成员),然后返回当前数组。
    target(必需):从该位置开始替换数据
    start(可选):从该位置开始读取数据,默认为0,如果是负值,表示倒数(右边第一位为-1)
    end(可选):到该位置前停止读取数据,默认为数组长度,如果是负值,表示倒数
  • array.sort()
    对数组进行排序,可接受参数,参数必须是函数,如果不没有参数 则是按照字符编码的顺序进行排序
    let arry = [10, 5, 40, 1000]
    console.log(arry.sort()) // [ 10, 1000, 40, 5 ]
    //如果数字想要按大小排列,可写入参数:
    let arr = [3,1,7];
    console.log(arr.sort((a,b) => a-b)) // [1,3,7]
  • array.push()
    把一个元素或多个元素增加到数组的末尾,返回值为新数组的长度`array.length`
  • array.pop()
    删除数组中最后一个元素,返回值为`删除的元素`
  • array.unshift()
    在数组的第一个元素前面添加一个元素或多个元素,返回值为新数组的长度`array.length`
  • array.shift()
    删除数组中第一个元素,返回值依然是被删除的元素
    不修改原数组的API有:
    slice/map/forEach/every/filter/reduce/entries/find/concat
  • array.slice(start,end)
    剪切数组,含头不含尾,返回剪切的数组
  • array.forEach(function(item,index))与array.map(function(item,index))
    两者都是对数组遍历,index表示数组索引,不是必须的参数
  • array.every(callback)
    用于检测数组中的所有元素是否满足指定条件,只有当数组中每一个元素都满足条件时,表达式返回true , 否则返回false
  • array.filter(callback)
    数组过滤,返回满足条件的元素组成的一个新数组
    let arr = [1,5,10,15];
    let arr1 = arr.filter(item => item > 5)
    console.log(arr1);
  • array.find(function(value,index,arr){...})
    1)find方法用于找出第一个符合条件的数组成员。它的参数是一个回调函数,数组中的每一个成员依次执行这个回调函数。
    2)如果找到第一个符合条件的成员,返回该成员。如果没有符合条件的,则返回undefined
    3)find方法的回调函数接受三个参数: value:当前值 | index:当前位置 | arr:原数组
    [1, 5, 10, 15].find(function(value, index, arr) {
    return value > 9;
    }) // 10
    注:查找成员位置 - arr.indexOf(ele)[如果不存在,则返回-1]
  • array.concat()
    用于连接两个或多个数组,返回值为连接后的新数组,原数组不变
    JS数组处理方式整理:https://segmentfault.com/a/1190000009233169

9. let、const 以及 var 的区别是什么?

  • let和const定义的变量不会出现变量提升,而var定义的变量会提升
  • let和const定义了一个拥有块级作用域属性的变量
  • let和const不允许重复声明变量(会抛出错误)
  • let和const定义的变量在定义之前使用,会抛出错误(形成暂时性死区),而var不会
  • const声明一个只读的常量。一旦声明,常量的值就不能改变(如果声明是一个对象,那么不能改变的是对象的引用地址)
    js声明变量var、let、const详解:https://segmentfault.com/a/1190000015325807

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

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

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

    typeof x; // ReferenceError(暂时性死区,抛错)
    let x;
    typeof y; // 值是undefined,不会报错
    var y;

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

11. 如何正确的判断this? 箭头函数的this是什么?

this的绑定规则有四种:默认绑定,隐式绑定,显式绑定,new绑定

  • 1. 函数是否在new中调用(new绑定),如果是,那么this绑定的是新创建的实例对象【前提是构造函数中没有返回对象或者是function,否则this指向返回的对象/function】

  • 2. 函数是否通过call,apply调用,或者用了bind,如果是,那么this绑定的就是指定的对象

  • 3. 函数是否在某个上下文对象中调用(隐式绑定),如果是,那么this绑定的就是那个上下文对象。一般为obj.foo()。

  • 4. 如果以上都不是,那么使用默认绑定。在严格模式下绑定到undefined,否则绑定到全局对象

  • 5. 如果把null或undefined作为this绑定对象传入call、apply或bind,这些值在调用时会被忽略,实际应用的是默认绑定规则

  • 6. 箭头函数没有自己的this,它的this继承于上一层代码块的this

12. 词法作用域和this的区别。

  • 词法作用域 -- 作用域是由书写代码时变量和函数声明的位置决定的
    通常来说,作用域一共有两种主要的工作模型:
    • 词法作用域
    • 动态作用域

词法作用域是大多数编程语言所采用的模式,而动态作用域仍有一些编程语言在用,例如 Bash 脚本。

而 JavaScript 就是采用的词法作用域,也就是在编程阶段,作用域就已经明确下来了。

  • this 机制跟动态作用域很相似,它是在调用时被绑定的,this 指向什么,完全取决于函数的调用位置

13. 谈谈你对JS执行上下文栈和作用域链的理解。

执行上下文是当前 JavaScript 代码被解析和执行时所在环境, JavaScript 中运行任何的代码都是在执行上下文中运行。
执行上下文分类:

  • 1)全局执行上下文
  • 2)函数执行上下文

执行上下文创建过程如下:

  • 创建变量对象:首先初始化函数的参数arguments,提升函数声明和变量声明。
  • 创建作用域链(Scope Chain):在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。
  • 确定this的值,即 ResolveThisBinding

JS执行上下文栈是一个存储函数调用的栈结构,遵循先进后出的原则。

  • JavaScript执行在单线程上,所有的代码都是排队执行。
  • 一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。
  • 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。
  • 浏览器的JS执行引擎总是访问栈顶的执行上下文。
  • 全局上下文只有唯一的一个,它在浏览器关闭时出栈。

作用域链: 无论是 LHS 还是 RHS 查询,都会在当前的作用域开始查找,如果没有找到,就会向上级作用域继续查找目标标识符,每次上升一个作用域,一直到全局作用域为止。

理解 JS 作用域链与执行上下文:
https://juejin.im/post/5abf5b5af265da23a1420833

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

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

闭包的作用有:

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