互联网寒冬之际,各大公司都缩减了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 ,$().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类型
常见的类数组:函数的参数arguments、DOM对象列表(比如通过 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.
- == 如果两者类型不同,首先需要进行类型转换。具体流程如下:
- 首先判断两者类型是否相同,如果相等,判断值是否相等.
- 如果类型不同,进行类型转换
- 判断比较的是否是 null 或者是 undefined, 如果是, 返回 true .
- 判断两者类型是否为 string 和 number, 如果是, 将字符串转换成 number
5.判断其中一方是否为 boolean, 如果是, 将 boolean 转为 number 再进行判断 - 判断其中一方是否为 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
- ES6 class 内部所有定义的方法都是不可枚举的;
- ES6 class 必须使用 new 调用;
- ES6 class 不存在变量提升;
- ES6 class 默认即是严格模式;
- 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的模块