以面试促学习—JavaScript篇(上)

160 阅读11分钟

本文不是面试题的简单罗列,而是将从典型的题目入手,剖析其背后知识点,灵活应对考题,避免处于无脑的刷10000道面试题,当第10001个面试题出现的时候还是不会尴尬境地。

JavaScript面试系列我将分三篇发文上篇(本文) 主要整理了JS中最基础的两个部分:基础语法变量类型和计算中篇我讲整理JavaScript 三座大山 相关面试题:原型和原型链作用域和闭包异步下篇将整理WEB-API(BOM、DOM、事件、Ajax、存储)、场景题手写代码题。

基础语法

基础中的基础,过一下,为后面铺垫!

1. let、const、var的区别

  1. let和const具有块级作用域,解决了ES5中的两个问题:内层变量可能覆盖外层变量用来计数的循环变量泄露为全局变量
  2. var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错。
  3. var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会(浏览器的全局对象是window,Node的全局对象是global)。
  4. var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。
  5. 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。
  6. 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...offor...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. 数组常用的方法

  • 非破坏性的(不会改变原数组的):

    1. concat(newArr):当前数组尾部拼接newArr,然后返回一个新数组;
    2. indexOf(item):在数组中寻找item,找到则返回其下标,找不到则返回-1
    3. includes(item):在数组中寻找item,找到则返回true,找不到则返回false
    4. join([flag]):将数组转化成以flag分割的字符串,并返回该字符串,不传flag则默认逗号隔开;
    5. reverse():翻转原数组,并返回已完成翻转的数组;
    6. slice(startIdx, endIdx):返回从startIdx 开始截取到endIdx的元素,但是不包括endIdx
    7. toString():将数组转化成字符串,并返回该字符串,逗号隔开;
    8. sort([fn]):对数组的元素进行排序(fn为可选的排序规则函数);
  • 破坏性的(原数组会改变):

    1. push():在尾部添加元素;
    2. pop():在尾部弹出一个元素;
    3. unshift():在头部添加元素;
    4. shift():在头部弹出一个元素;
    5. splice(startIdx, delleteCount, newItem1, newItem2...):删除从startIdx位置开始的deleteCount个元素,并在尾部新增newItem1newItem2...;

5. 字符串常用方法

  1. charAt(idx):返回idx索引位置处的字符;
  2. concat(newStr):在原字符串尾部拼接上newStr;
  3. indexOf(str):返回一个字符str在字符串中首次出现的位置;
  4. lastIndexOf(str):返回一个字符str在字符串中最后一次出现的位置;
  5. slice([startIdx], [endIdx]):提取从startIdx位置到endIdx位置的字符串;
  6. split(flag):使用指定的分隔符flag将一个字符串拆分为多个子字符串数组并返回;
  7. substr(startIdx, [length]):从startIdx位置开始截取长度为length的字符串;
  8. substring(startIdx, endIdx):截取从startIdx位置到endIdx位置的字符串,不包含endIdx;
  9. toLowerCase()/toUpperCase():转换成小写/大写;
  10. includes(str)/startsWith(str)/endsWith(str):检查字符串是否包含str/以str开头/以str结尾,返回boolean;
  11. replace(aim, res):第一个参数aim是需要替换掉的字符或者一个正则的匹配规则,第二个参数res是需要替换进去的字符,也可以是一个回调函数。
  12. repeat(count):返回一个使原字符串重复count次的字符串;
  13. match(reg):在字符串内检索值,reg可以是字符串或正则表达式,返回一个检索结果的数组;

6. 如何遍历函数的arguments参数?

arguments是一个对象,它的属性是从 0 开始依次递增的数字,有length等属性,与数组相似;但是它却没有数组常见的方法属性,如forEachreduce等,所以叫它们类数组

要想遍历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)。存储在不同位置有什么区别呢?

值类型示例:

  1. 例如声明变量let a = 100let b = 150,就在栈中依次开辟两块空间:
栈(stack)
keyvalue
a100
b150
  1. 然后进行操作b = 200,就直接在栈中修改Key为b所对应的Value。
栈(stack)
keyvalue
a100
b200

引用类型示例:

  1. 声明变量obj1 = { name: "WEB实习僧" },会在堆中存储变量的值,然后在栈中存储对应堆的内存地址。
栈(stack)
keyvalue
obj1内存地址1
堆(heap)
keyvalue
内存地址1{ name: 'WEB实习僧' }
  1. 然后操作obj2 = obj1,计算机只在栈中将obj1的内存地址赋值给obj2,于是obj1和obj2指向相同的内存地址1.
栈(stack)
keyvalue
obj1内存地址1
obj2内存地址1
堆(heap)
keyvalue
内存地址1{ name: "WEB实习僧" }
  1. 修改obj2的值:obj2.name = "Hello World",计算机根据栈中obj2的内存地址,去堆中修改对应的值。
栈(stack)
keyvalue
obj1内存地址1
obj2内存地址1
堆(heap)
keyvalue
内存地址1{ name: "Hello World" }
  1. 所以虽然没有修改obj1,但输出obj1时发现其值也变了,这就是因为obj1和obj2的值存储在同一内存地址中。

2. typeof运算符都能识别什么类型?

  1. 识别所有的值类型(除了null);
  2. 识别函数;
  3. 识别是否为引用类型(不能在细分);
// 值类型:完全能够区分
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。