本文不是面试题的简单罗列,而是将从典型的题目入手,剖析其背后知识点,灵活应对考题,避免处于无脑的刷10000道面试题,当第10001个面试题出现的时候还是不会尴尬境地。
JavaScript面试系列我将分三篇发文,上篇(本文) 主要整理了JS中最基础的两个部分:基础语法
、变量类型和计算
,中篇我讲整理JavaScript 三座大山 相关面试题:原型和原型链
、作用域和闭包
、异步
。下篇将整理WEB-API
(BOM、DOM、事件、Ajax、存储)、场景题
及手写代码
题。
基础语法
基础中的基础,过一下,为后面铺垫!
1. let、const、var的区别
- let和const具有块级作用域,解决了ES5中的两个问题:
内层变量可能覆盖外层变量
、用来计数的循环变量泄露为全局变量
; - var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错。
- var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会(浏览器的全局对象是window,Node的全局对象是global)。
- var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。
- 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。
- va r和let创建的变量是可以更改指针指向(可以重新赋值),但const声明的变量是不允许改变指针的指向。
对于第6点,const保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。所以对于基本数据类型来说,const声明的就无法改变了,而引用类型的是可以修改的。
2. for...in与for...of的用法
for...in
主要是为了遍历对象而生,返回对象的键。也可以遍历数组和字符串,返回索引;
for…of
是ES6新增的遍历方式,只要数据结构具有iterator 接口,就可以使用 for...of遍历,如:数组、Set 和 Map 结构、某些类似数组的对象(arguments、DOM NodeList)、 Generator对象,以及字符串。
那么如何使用for...of遍历对象呢?
我们知道for…of只能遍历有iterator接口的数据类型,所以我们给对象加一个[Symbol.iterator]属性,并指向一个迭代器即可。
var obj = {
name: "WEB实习僧",
age: "18"
};
obj[Symbol.iterator] = function(){
var keys = Object.keys(this);
var count = 0;
return {
next(){
if(count < keys.length){
return { value: keys[count++], done: false };
}else{
return { value: undefined, done: true };
}
}
}
};
for(var k of obj){
console.log(k);
}
3. 各种数组的遍历方式及区别
我们这里对for、while、do...wihlie、不做过多说明。
方法 | 特点 |
---|---|
forEach() | 数组方法,不改变原数组,没有返回值 |
map() | 数组方法,不改变原数组,有返回值,可链式调用 |
filter() | 数组方法,过滤数组,返回包含符合条件的元素的数组,可链式调用 |
for...of | for...of遍历具有Iterator迭代器的对象的属性,返回的是数组的元素、对象的属性值,不能遍历普通的obj对象,将异步循环变成同步循环 |
every() 和 some() | 数组方法,some()只要有一个是true,便返回true;而every()只要有一个是false,便返回false. |
find() 和 findIndex() | 数组方法,find()返回的是第一个符合条件的值;findIndex()返回的是第一个返回条件的值的索引值 |
reduce() 和 reduceRight() | 数组方法,reduce()对数组正序操作;reduceRight()对数组逆序操作 |
下面了解下ES2018中引入方法:for await...of
,被称为异步迭代器,主要用来遍历异步对象,只能在 async function
内使用。
function Gen (time) {
return new Promise((resolve,reject) => {
setTimeout(function () {
resolve(time)
},time)
})
}
async function test () {
let arr = [Gen(2000),Gen(100),Gen(3000)]
for await (let item of arr) {
console.log(Date.now(),item)
}
}
test()
4. 数组常用的方法
-
非破坏性的(不会改变原数组的):
- concat(newArr):当前数组尾部拼接newArr,然后返回一个新数组;
- indexOf(item):在数组中寻找item,找到则返回其下标,找不到则返回
-1
; - includes(item):在数组中寻找item,找到则返回
true
,找不到则返回false
; - join([flag]):将数组转化成以flag分割的字符串,并返回该字符串,不传flag则默认逗号隔开;
- reverse():翻转原数组,并返回已完成翻转的数组;
- slice(startIdx, endIdx):返回从
startIdx
开始截取到endIdx
的元素,但是不包括endIdx
; - toString():将数组转化成字符串,并返回该字符串,逗号隔开;
- sort([fn]):对数组的元素进行排序(fn为可选的排序规则函数);
-
破坏性的(原数组会改变):
- push():在尾部添加元素;
- pop():在尾部弹出一个元素;
- unshift():在头部添加元素;
- shift():在头部弹出一个元素;
- splice(startIdx, delleteCount, newItem1, newItem2...):删除从
startIdx
位置开始的deleteCount
个元素,并在尾部新增newItem1
、newItem2
...;
5. 字符串常用方法
- charAt(idx):返回idx索引位置处的字符;
- concat(newStr):在原字符串尾部拼接上newStr;
- indexOf(str):返回一个字符str在字符串中首次出现的位置;
- lastIndexOf(str):返回一个字符str在字符串中最后一次出现的位置;
- slice([startIdx], [endIdx]):提取从startIdx位置到endIdx位置的字符串;
- split(flag):使用指定的分隔符flag将一个字符串拆分为多个子字符串数组并返回;
- substr(startIdx, [length]):从startIdx位置开始截取长度为length的字符串;
- substring(startIdx, endIdx):截取从startIdx位置到endIdx位置的字符串,不包含endIdx;
- toLowerCase()/toUpperCase():转换成小写/大写;
- includes(str)/startsWith(str)/endsWith(str):检查字符串是否包含str/以str开头/以str结尾,返回boolean;
- replace(aim, res):第一个参数aim是需要替换掉的字符或者一个正则的匹配规则,第二个参数res是需要替换进去的字符,也可以是一个回调函数。
- repeat(count):返回一个使原字符串重复count次的字符串;
- match(reg):在字符串内检索值,reg可以是字符串或正则表达式,返回一个检索结果的数组;
6. 如何遍历函数的arguments参数?
arguments
是一个对象,它的属性是从 0 开始依次递增的数字,有length
等属性,与数组相似;但是它却没有数组常见的方法属性,如forEach
、reduce
等,所以叫它们类数组
。
要想遍历arguments参数,只需要将它转换成真正的数组即可,主要方法有:
// 调用数组的slice/splice/concat方法:
const newArgu1 = Array.prototype.slice.call(arguments);
const newArgu2 = Array.prototype.splice.call(arguments, 0);
const newArgu3 = Array.prototype.splice.call([], arguments);
// 使用Array.from方法:
const newArgu4 = Array.from(arguments)
变量类型和计算
不会变量,别说你会JS!
1. JavaScript的数据类型有哪些,它们有什么不同?
JavaScript数据类型大方向上分为值类型
和引用类型
。
常见的值类型有如下5种:
let a; // undefined
let b = 'WEB实习僧' // string
let c = 10086; // number
let d = true; // boolean
let e = Symbol('hello'); // symbol
let f = null; // null
常见的引用类型有如下4种:
let obj = { name: 'WEB实习僧' }; // object
let arr = ['hello', 'world']; // array
function f() {}; // function
两种类型的区别在于存储位置的不同:值类型存贮才栈(stack)中,引用类型存储在堆中(heap)。存储在不同位置有什么区别呢?
值类型示例:
- 例如声明变量
let a = 100
,let b = 150
,就在栈中依次开辟两块空间:
栈(stack) | |
---|---|
key | value |
a | 100 |
b | 150 |
- 然后进行操作
b = 200
,就直接在栈中修改Key为b所对应的Value。
栈(stack) | |
---|---|
key | value |
a | 100 |
b | 200 |
引用类型示例:
- 声明变量
obj1 = { name: "WEB实习僧" }
,会在堆中存储变量的值,然后在栈中存储对应堆的内存地址。
栈(stack) | |
---|---|
key | value |
obj1 | 内存地址1 |
堆(heap) | |
---|---|
key | value |
内存地址1 | { name: 'WEB实习僧' } |
- 然后操作
obj2 = obj1
,计算机只在栈中将obj1的内存地址赋值给obj2,于是obj1和obj2指向相同的内存地址1.
栈(stack) | |
---|---|
key | value |
obj1 | 内存地址1 |
obj2 | 内存地址1 |
堆(heap) | |
---|---|
key | value |
内存地址1 | { name: "WEB实习僧" } |
- 修改obj2的值:
obj2.name = "Hello World"
,计算机根据栈中obj2的内存地址,去堆中修改对应的值。
栈(stack) | |
---|---|
key | value |
obj1 | 内存地址1 |
obj2 | 内存地址1 |
堆(heap) | |
---|---|
key | value |
内存地址1 | { name: "Hello World" } |
- 所以虽然没有修改obj1,但输出obj1时发现其值也变了,这就是因为obj1和obj2的值存储在同一内存地址中。
2. typeof运算符都能识别什么类型?
- 识别所有的值类型(除了null);
- 识别函数;
- 识别是否为引用类型(不能在细分);
// 值类型:完全能够区分
console.log(typeof 1); // number
console.log(typeof NaN); // number
console.log(typeof true); // boolean
console.log(typeof 'hello'); // string
console.log(typeof undefined); // undefined
console.log(typeof Symbol()); // symbol
// typeof null为什么返回object,下一题详细说明
console.log(typeof null); // object
// 引用类型:只能分辨函数
console.log(typeof function(){}); // function
console.log(typeof {}); // object
console.log(typeof []); // object
3. []+[]
、[]+{}
、{}+[]
、{}+{}
结果
隐式类型转换是个非常基础的大问题,在问题在我一篇文章有单独论述:你真的懂JS隐式类型转换吗。
明白了隐式类型转换后,对于[]+[]
很简单,等同于""+""
所以结果为""
。
同理[]+{}
等同于""+"[object Object]"
,结果为[object Object]
。
有了上边的,对于{}+[]
是不是很快知道它等同于"[object Object]"+""
,所以结果也为"[object Object]"
?No,大错特错! 我们在Chrome浏览器控制台输入{}+[]
,结果竟然是0
,原理很简单,Chrome把{}
解析成了代码块来执行,相当于{};+[]
,然后执行+[]
,对于+[]
的结果肯定是0
。
现在讨论{}+{}
,有了前边的铺垫,我们很快就能得出答案是:NaN
。为什么?因为Chrome把{}
解析成了代码块来执行,然后执行+{}
,结果肯定是NaN
。这个答案是对的也不是对的! Chrome浏览器中{}+{}
的结果是"[object Object][object Object]
,在其他浏览器中结果是NaN
,是不是很神奇,为什么Chrome中输出的不一样呢呢?原因是Chrome浏览器会对左边是{
右边是}
的代码用()
包起来进行求值,所以等同于 "[object Object]"+"[object Object]"
,就有了上边的结果。
JS中,执行+ x
时会把x
转换成number
类型,常见的有:
console.log(+ []); // 0
console.log(+ [1]); // 1
console.log(+ [1, 2]); // NaN
console.log(+ {}); // NaN
console.log(+ false); // 0
console.log(+ true); // 1
console.log(+ null); // 0
console.log(+ undefined); // NaN
console.log(+ 10n); // Uncaught TypeError: can't convert BigInt to number
console.log(+ Symbol("1")); // Uncaught TypeError: can't convert symbol to number
4. falsely变量有哪些?
先来了解下truly变量和falsely变量:
- truly变量:
!!a === true
的变量; - falsely变量:
!!a === falsely
的变量;
以下是falsely变量,除此之外都是truly变量:
!!0 === false
!!NaN === false
!!"" === false
!!null === false
!!undefined === false
!!false === false
思考: 什么时候用==
,什么时候用===
?
除了与null进行比较之外,其他一律用===
, 例如:
const user = { x: "WEB实习僧" };
// 相当于 (user.y === null || user.y === undefined)
if (user.y == null) {}
上边user.y == null
相当于user.y == null || user.y == undefined
。
5. Object.is()与===
、==
的区别
- 使用双等号(==)进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。
- 使用三等号(===)进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。
- 使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的。
6. 写个深拷贝吧
由第1题我们知道,一个新的对象对原始对象的属性值进行精确地拷贝,如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值,如果是引用数据类型,拷贝的就是内存地址。如果其中一个对象的引用内存地址发生改变,另一个对象也会发生变化,这就是浅拷贝
。所以为了拷贝一个对象后,修改这个对象使拷贝的对象不受影响,就有了深拷贝
。
function deepClone(obj = {}) {
if (typeof obj !== 'object' || obj == null) {
// obj 是 null ,或者不是对象和数组,直接返回
return obj
}
// 初始化返回结果
let result
// instanceof是什么,下一节会讲,这里用来判断obj是否为数组
if (obj instanceof Array) {
result = []
} else {
result = {}
}
for (let key in obj) {
// hasOwnProperty是什么,下一节会讲,这里用来保证key不是原型的属性
if (obj.hasOwnProperty(key)) {
// 递归调用!!!
result[key] = deepClone(obj[key])
}
}
// 返回结果
return result
}
有关深拷贝的写法还有很多,但对于面试来说,这一个就够了,其他写法我会在以后单独写一篇文章聊聊JavaScript需要会手写的东西。
7. 两个JS迷惑行为
迷惑1: 为什么0.1 + 0.2 = 0.30000000000000004
?
关于JS精度问题,在我上一篇文章中有详细介绍:从0.1+0.2 !== 0.3聊聊JavaScript精度问题。
迷糊2: 为什么typeof null
为什么返回object
?
null作为一个基本数据类型为什么会被typeof运算符识别为object类型呢? 这个bug是第一版Javascript留下来的,javascript中不同对象在底层都表示为二进制,而javascript 中会把二进制前三位都为0的判断为object类型,而null的二进制表示全都是0,自然前三位也是0,所以执行typeof时会返回 'object'。 《你不知道的javascript(上卷)》
这里有五种标志位:
000: object - 当前存储的数据指向一个对象。
1: int - 当前存储的数据是一个 31 位的有符号整数。
010: double - 当前存储的数据指向一个双精度的浮点数。
100: string - 当前存储的数据指向一个字符串。
110: boolean - 当前存储的数据是布尔值。
如果最低位是 1,则类型标签标志位的长度只有一位;如果最低位是 0,则类型标签标志位的长度占三位,为存储其他四种数据类型提供了额外两个 bit 的长度。
有两种特殊数据类型:
- undefined的值是
-2^30
; - null 的值是机器码
NULL 指针
(null 指针的值全是 0);
那也就是说null的类型标签也是000,和Object的类型标签一样,所以会被判定为Object。