JavaScript八股文

JavaScript 变量

var、let、const 的差异?

相同点

varletconst 三者都可以声明变量。变量可以看作盒子,变量名就是盒子名称,值是放在盒子里的东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// var 声明变量,初始值可选
var name;
var name = "Lucy";

// let 声明变量,初始值可选
let age;
let age = 12;

// const 声明常量,必须要赋初始值
const city = "Beijing";
// 如果给常量重新赋值会报错
const city = "Shanghai"; // Uncaught SyntaxError: Identifier 'city' has alre ady been declared

// 如果常量的值是对象(数组),不可以修改常量指向的引用,但是可以修改引用的值
const cities = ["Beijing"];
cities[0] = ["Shanghai"]; // "Shanghai"

差异

varletconst 三者的差异:

定义 作用域 暂时性死区 重复声明变量 全局属性
const 声明常量 块级作用域
let 声明变量(ES6 新增) 块级作用域
var 声明变量 函数作用域(函数内)

注:

暂时性死区:Temporal dead zone(TDZ)

全局属性:是否会被添加到 windowglobalThis 等对象中

暂时性死区

从一个代码块的开始直到代码执行到声明变量的行之前,letconst 声明的变量都处于“暂时性死区中。简单理解:letconst 只能先声明再访问。如下面的代码:

1
2
3
4
5
6
7
8
9
// 访问 person 变量
// 变量 person 使用 let 声明,声明不会提升,因此此处访问会报错
console.log(person); // ReferenceError: person is not defined

// 声明 person
let person = {
name: "Lucy"
};

同理,使用 const 声明变量,如果在声明前使用,表现与 let 一致。var 声明的全局变量会进行变量提升:

1
2
3
4
5
console.log(person); // undefined

var person = {
name: "Lucy"
};

全局属性

var 声明的变量会被添加到全局对象中,可以使用 window globalThis 访问,letconst 声明的全局变量则不会添加到全局对象中。

1
2
3
4
5
6
7
8
9
10
11
var name = "Lucy";
console.log(window.name); // "Lucy"
console.log(globalThis.name); // "Lucy"

const age = 12;
console.log(window.age); // undefined
console.log(globalThis.age); // undefined

let gender = "female";
console.log(window.gender); // undefined
console.log(globalThis.gender); // undefined

小练习

以下代码输出结果是什么?

1
2
3
4
5
const obj = { prop: 0 };
obj.prop = obj.prop + 1;

console.log(obj.prop); // 1. 打印结果是什么?
obj = {}; // 2. 执行结果是什么?

答案:1 处输出 1, 2 处报错: “Uncaught TypeError: Assignment to constant variable”。这与 const 的特性有关,const 仅仅意味着变量名和值的绑定是不可变的,但是它的值可以是可变的,如 1 处 的值 { prop: 0 } 就可以更改。因此,我们可以得出结论:1 处 obj 的值是对象,可以改变 obj 的属性,然而不能重新给 obj 赋新的值(2 处)。


谈谈作用域?

作用域

作用域是当前的执行上下文,值和表达式在其中“可见”或可被访问。

静态作用域和动态作用域

作用域分为静态作用域(又称为词法作用域)和动态作用域:

  • 静态意味着它与代码的位置有关,与执行代码时的环境无关。JavaScript 采用的是静态作用域。
  • 动态即运行时,代码执行时确定的。

JavaScript 作用域

JavaScript 的作用域可以分为以下四种:

  • 全局作用域:脚本模式运行所有代码的默认作用域

  • 函数作用域:由函数创建的作用域

  • 块级作用域:用一对花括号(一个代码块)创建出来的作用域

  • 模块作用域:模块模式中运行代码的作用域

接下来,我们分析一下各种作用域的特点。

全局作用域

JavaScript 变量作用域是嵌套的,它们形成树:

  • 最外面的作用域是树的根部,也叫全局作用域。

  • 被最外层作用域包含的作用域是根的子孙,如各种嵌套的代码块形成的作用域。

全局作用域中的变量称为全局变量,可以在任何作用域内访问。有两种全局变量:

  • 全局声明变量(declarative variables)是普通变量,在最顶级由 constletclass 声明的变量。

  • 全局对象(object variables)是存储在全局对象中的属性:

    • 在最顶级由 varfunction 声明后创建的变量
    • 全局对象可以通过 globalThiswindow 访问,它可以对全局对象变量进行增删改查

全局属性 globalThis 包含全局的 this 值,类似于全局对象。globalThis 提供了一个标准的方式来获取不同环境下的全局 this 对象(也就是全局对象自身),它可以在任何环境下使用。

下面的代码可以帮助我们更好理解 globalThis 和两种全局变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
const declarativeVariable = 'd';
var objectVariable = 'o';
</script>

<script>
// 所有脚本共享同样的顶级作用域
console.log(declarativeVariable); // 'd'
console.log(objectVariable); // 'o'

// 不是所有变量都会创建为全局对象的属性
// 此处最顶层 const 创建的是全局声明变量,不是全局对象,因此 window 对象下访问为 undefined
console.log(window.declarativeVariable); // undefined
console.log(window.objectVariable); // 'o'
// globalThis 同 window 表现一致
console.log(globalThis.declarativeVariable); // undefined
console.log(globalThis.objectVariable); // 'o'
</script>

观察以上的代码,就能明白为什么最顶层 const 创建的变量,使用 window 访问依然是 undefined

函数作用域

函数内声明的变量,只能在函数作用域范围内访问。

1
2
3
4
5
function scope() {
var address = "Beijing";
}

console.log(address); // Uncaught ReferenceError: address is not defined

块级作用域

作用域对变量来说,可以简单理解为程序能够访问到变量的范围,超过作用域的就无法访问。

letconst

letconst 支持块级作用域。如果在代码块 {...} 中使用 letconst 声明一个变量,那么这个变量只在该代码块中可见。

1
2
3
4
5
6
7
8
9
10
11
12
13
{ 
// 作用域 A,可以访问变量 x
const x = 0;
console.log(x); // 0
{
// 作用域 B,可以访问 x、y
const y = 1;
console.log(x); // 0
console.log(y); // 1
}
}
// 作用域 A 外,不能访问 x、y
console.log(x); // 报错:Uncaught ReferenceError: x is not defined

我们分析一下上面的代码:

  • 作用域 A 是变量 x 的作用域
  • 作用域 B 是作用域 A 的内部作用域
  • 作用域 A 是作用域 B 外部作用域

每个变量可访问的范围是它所在的作用域以及该作用域所嵌套的外部作用域。letconst 声明的变量是块级的,因此它们作用域始终在块中。同一作用域内,不允许声明同名变量。如下示例代码用 const 声明同名变量:

1
2
3
4
{
const x = 1;
const x = 2; // 报错:Uncaught SyntaxError: Identifier 'x' has already been declared
}

不同作用域下可以使用同名变量:

1
2
3
4
5
6
{
const x = 1;
{
const x = 2;
}
}

class

class 声明的类也支持块级作用域。如下面代码,在全局作用域中声明了 Animal

1
2
3
class Animal {}

console.log(Animal); // class Animal {}

如果在代码块 {...} 中创建 Animal,此时在代码块外部就无法访问 Animal了。

1
2
3
4
5
6
7
{
class Animal {}
console.log(Animal) // class Animal {}
}

console.log(Animal); // Uncaught ReferenceError: Animal is not defined

模块作用域

每个 ECMAScript 模块(ES6 Modules)都有自己的作用域,因此,在顶级模块中声明的变量不是全局的。如下图所示:

总结

作用域是值或者表达式的可访问范围。分为静态作用域(词法作用域)和动态作用域,JavaScript 采用的是静态作用域。其中,分为四种不同的作用域:全局作用域、函数作用域、块级作用域以及模块作用域。

除此之外,文中还提到了各种声明与作用域的关系,下面总结一下各种声明的异同。我们从以下四个方面来看各种声明的异同:

  • 作用域
  • 暂时性死区:变量何时可以访问?一些变量在进入作用域后可尽快被访问,但有的必须等到代码执行到它们声明时才可以访问。通俗说法是,这个变量是否可以先访问再声明。暂时性死区(TDZ, Temporal Dead Zone) 就是变量在进入作用域和执行声明前的一段时间。在这段时间内,访问变量会报错。
  • 重复:变量是否可以重复声明(同级作用域下)
  • 全局属性:声明的变量是否会被添加到全局对象中
定义 作用域 暂时性死区 重复声明变量 全局属性
const 声明常量 块级作用域
let 声明变量(ES6 新增) 块级作用域
function 声明常量 块级作用域
class 块级作用域
var 声明变量 函数作用域(函数内)

什么是变量提升?

变量提升和上文提到触发时间有关。我们知道,varfunction 声明的变量可以在声明前访问,这就是因为变量提升的缘故。

当 JavaScript 引擎执行代码时,创建了全局执行上下文,它有两个阶段:

  • 创建(准备工作)
  • 执行

在创建阶段,JavaScript 引擎将 varfunction 声明移到了顶层,这就是 JavaScript 的变量提升。

var 关键字

我们先看一段代码:

1
2
console.log(counter); // undefined
var counter = 1;

在这段代码中,我们在声明前访问 counter 变量,并未报错。这是变量提升的缘故。

function 提升

var 一样,函数声明也会提升:

1
2
3
4
5
6
7
8
9
let x = 20,
y = 10;

let result = add(x, y);
console.log(result); // 30

function add(a, b) {
return a + b;
}

除此之外,let 关键字、函数表达式、箭头函数等均不会变量提升。

处理相同的变量名或者函数名

代码中出现相同的变量或者函数怎么办?我们知道 var 声明的同名变量,后者会覆盖前者,如果是函数呢?先看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let x = 20,
y = 10;

function add(a, b) {
return a + b;
}

let result = add(x, y);
console.log("result " + result); // "result 60"

function add(a) {
return a + 40;
}

let result1 = add(x);
console.log("result1 " + result); // "result1 60"

从上面的结果可以看到,两个同名函数 add,定义在后面的函数覆盖了前面的函数,因此 resultresult1 的结果都是执行后面的函数后返回的结果。

因此,代码中出现同名的变量名或者函数名,都是后者覆盖前者。

小练习

下面的代码输出的结果是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
let x = 20,
y = 10;

let result = add(x);
console.log("result1 " + result); // "result1 60"

var add = function(a, b) {
return a + b;
}

function add(a) {
return a + 40;
}

这里考察的是同名变量和函数的提升,后面的会覆盖前面的,因此执行的是最后一个函数 add


JavaScript 数据类型

JavaScript 数据类型有哪些?

面试高频指数:★★★★★

JavaScript 中有八种基本的数据类型(前七种为基本数据类型,也称为原始数据类型,后一种 Object 为复杂数据类型,也称为非原始数据类型或引用类型)。

  • 其中原始数据类型:
    • number 用于任何类型的数字:整数或浮点数,在 ±(2(^53)-1) 范围内的整数。
    • bigint 用于任意长度的整数。
    • string 用于字符串:一个字符串可以包含 0 个或多个字符,所以没有单独的字符类型。
    • boolean 用于 truefalse
    • null 用于未知的值 —— 只有一个 null 值的独立类型。
    • undefined 用于未定义的值 —— 只有一个 undefined 值的独立类型。
    • symbol 用于唯一的标识符。
  • 以及一种非原始数据类型:
    • Object 用于更复杂的数据结构。以下类型都是对象:
      • Function(函数)
      • Array(数组)
      • Date(日期)
      • RegExp(正则表达式)

原始数据类型和引用数据类型的区别?

面试高频指数:★★★★☆

JavaScript 包含两种不同类型的值:

  • 原始数据类型(Primitive values)

  • 引用数据类型(Reference values)

栈内存和堆内存

当定义一个变量的时候,JavaScript 引擎会为变量分配两种内存:栈内存和堆内存。

静态值在编译阶段有固定的大小,静态值有:

  • 原始值:NullUndefinedBooleanNumberStringSymbolBigInt
  • 引用值:是对象的引用。

静态值有固定的大小,不能改变。JavaScript 引擎为它们分配一片固定的内存,并存储在栈上。例如:

1
2
let name = "John";
let age = 25;

因为 nameage 都是原始值类型,JavaScript 引擎将它们存储在栈上,如下图所示:

JavaScript 将对象(Object) 存储在堆(heap)上。

1
2
3
4
let person = {
name: "John",
age: 25
};

内存如下图:

JavaScript 引擎在堆内存上创建了一个新的对象,同时它和栈内存上的 person 变量连接。因此,我们说 person 变量是对象的引用。

动态属性

一个引用值允许我们添加、修改和删除属性,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let person = {
name: "John",
age: 25
};

// 添加属性 ssn
person.ssn = "123-45";

// 修改 name
person.name = "John Doe";

// 删除属性 age
delete person.age;

console.log(person); // {name: 'John Doe', ssn: '123-45'}

JavaScript 也允许在原始值上添加属性,但这个属性不会起作用。

1
2
3
let name = "John";
name.alias = "Knight";
console.log(name.alias); // undefined

复制值

原始值

对于原始值来说,JavaScript 引擎创建一个值的副本,并将值赋给新的变量。

1
2
3
let age = 25;
let newAge = age;
console.log(age, newAge); // 25 25

过程如下:

  • 首先,声明一个变量 age,并将 25 赋值给它。

  • 其次,声明另一个新的变量 newAge,将 age 赋值给 newAge,JavaScript 引擎将 25 复制了一份,给了新变量。

如下图:

因此,对两个变量的操作不会互相影响。

1
2
3
4
5
let age = 25;
let newAge = age;

newAge = newAge + 1;
console.log(age, newAge); // 25 26

如下图:

引用值

对于引用值来说,复制的值指向的是同一个对象,因此操作的是也是同一个对象。

当我们将一个引用值从一个变量赋值给另一个变量,JavaScript 引擎创建一个引用,因此两个变量都是指向堆内存中的同一个对象。意味着,你修改其中一个,另一个也会被修改。

1
2
3
4
5
6
7
8
9
10
let person = {
name: "John",
age: 25
}

let member = person;
member.age = 26;

console.log(person); // {name: 'John', age: 26}
console.log(member); // {name: 'John', age: 26}

如下图所示:

修改前:

修改后:

image.png

总结

  • JavaScript 有两种类型的值:原始值和引用值

  • 引用类型的值可以对它的属性做增删改查,原始值不行

  • 从一个变量复制原始值到另一个变量,会创建一个独立的值的备份,意味着修改一个变量不会影响到另一个变量

  • 从一个变量复制引用值到另一个变量,两个变量会指向同一个对象,意味着通过一个变量修改对象将会影响到另一个对象。

小练习

下面代码的输出结果是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
let person = {
name: "John",
age: 25
}

function increaseAge(obj) {
obj.age += 1;
obj = {name: "Jame", age: 22}
console.log(obj); // 1
}

increaseAge(person);
console.log(person); // 2

答案:1 处是:{ name: "Jame", age: 22 }, 2 处是 { name: 'John', age: 26 }。两个变量的内存示意图如下:

image.png

实际上,obj = {name: "Jame", age: 22} 使得 obj 重新指向了一个新的引用的,对象在堆内存中重新分配一块内存,并让 obj 指向它。 因此 console.log(person) 的结果是 { name: "Jame", age: 22 }

原始类型和引用类型在内存中的分配与函数参数传递有联系。函数传参都是值传递,只不过根据值的类型不同有所区别。

由上可知:

  • JavaScript 实参都是传值
  • 函数实参会在函数中创建新的局部变量。

为什么 0.1 + 0.2 !== 0.3 ?

面试高频指数:★★★★☆

这是一道经典的面试题:

1
0.1 + 0.2 === 0.3; // false

这里涉及到 JavaScript 中的数字类型。下面是 Number 的定义:

MDN 中对 Number 的定义如下:

根据语言规范,JavaScript 采用“遵循 IEEE 754 标准的双精度 64 位格式”(”double-precision 64-bit format IEEE 754 values”)表示数字。

为什么会这样?

简单地说,0.1 和 0.2 的二进制表示形式是不准确的,所以它们相加时,结果不是精确的 0.3, 而是非常接近的值:0.30000000000000004。

这是和 JavaScript 采用“遵循 IEEE 754 标准的双精度 64 位格式”有关。

  • sign bit(符号): 用来表示正负号
  • exponent(指数): 用来表示次方数
  • mantissa(尾数):用来表示精确度

image.png

在这个标准下:

  • 1 位存储符号(Sign),0 表示正数, 1 表示负数。

  • 用 11 位存储指数,指数必须是“有符号”的值,这里使用了偏差指数,即存储 E + bias 的值。对于 11 位来说,bias 的值是 2^(11-1) - 1,也就是 1023。11 位无符号整数的值的范围是 0 到 2^11(2047),由于全 0 和 全 1 的指数值是为特殊数字保留的,所以可用的指数是从 1 到 2046。减去指数偏差值 1023, 就能得到指数的实际范围,即从 -1022 到 +1023。

  • 1 - 1023 = -1022
    2046 - 1023 = +1023
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

    - 用 52 位存储 Fraction。

    此时,我们再来看 0.1 + 0.2 的转换过程,举个例子,拿 0.1 来看:
    0.1 对应的二进制是 1 * 2^-4 * 1.1001100110011……
    符号位:0
    E + bais: -4 + 1023 = 1019
    Fraction: 1001100110011……
    对应的 64 位完整表示如下图:

    ![image.png](https://pic.leetcode.cn/1675248450-nFKGop-image.png)

    同理,0.2 的完整表示是:

    ![image.png](https://pic.leetcode.cn/1675248480-DpROri-image.png)

    所以,**当 0.1 存下来的时候,就发生了精度丢失,当我们用浮点数进行运算的时候,使用的其实是精度丢失后的数。**

    当我们对两个数字求和时,它们的“精度损失”会叠加起来。这就是为什么 0.1 + 0.2 不等于 0.3。

    ### 如何解决?

    **方法一:`toFixed(n)`**

    我们可以借助方法 `toFixed(n)` 对结果进行舍入。

    let sum = 0.1 + 0.2; alert(sum.toFixed(2)); // 0.30
    1
    2
    3
    4
    5
    6

    注意:`toFixed `总是返回一个字符串。我们可以使用一元加号将其强制转换为一个数字:

    ```js
    let sum = 0.1 + 0.2;
    alert( +sum.toFixed(2) ); // "0.30"

方法二:将数字临时乘以 100(或更大的数字),将其转换为整数,进行数学运算,然后再除回。

1
alert( (0.1 * 100 + 0.2 * 100) / 100 ); // 0.3

方法三:使用 Number.EPSILON。如果两个数的精度损失在允许范围内,则可以认为两个数是相等的。

Number.EPSILON 属性表示 1 与 Number 可表示的大于 1 的最小的浮点数之间的差值。

1
2
3
4
5
6
7
8
9
function numbersCloseEnoughToEqual(n1, n2) {
return Math.abs(n1 - n2) < Number.EPSILON;
}

let a = 0.1 + 0.2;
let b = 0.3;

numbersCloseEnoughToEqual(a, b); // true
numbersCloseEnoughToEqual(0.0000001, 0.0000002); // false

小练习

下面代码的打印结果是什么?

1
console.log(9999999999999999); // 16位

答案:输出结果 10000000000000000(17位)。

这也是因为精度损失。有 64 位来表示该数字,其中 52 位可用于存储数字,但这还不够。所以最不重要的数字就消失了。

JavaScript 不会在此类事件中触发 error。它会尽最大努力使数字符合所需的格式,但不幸的是,这种格式不够大到满足需求。

【参考资料】
双精度浮点数
Double (IEEE754 Double precision 64-bit)
数字类型


谈谈 undefined 和 null ?

面试高频指数:★★★★☆

undefinednull 都是基本数据类型。它们的定义是:

  • undefined 意味着变量已经声明了但是没有赋值。

  • null 是空值,可以作为对象的初始值。

undefined 不是 undeclared

  • undeclared 是指变量从未在代码中出现.使用未声明的变量就会报错:”ReferenceError: cat is not defined“。

  • undefined 则是声明了但是值是 undefined 或者值并不存在。

如何获取安全的 undefined 值?

因为 undefined 是一个标识符,所以可以被当作变量来使用和赋值,但是这样会影响 undefined 的正常判断。

可以使用 void 0 获得安全的值。

1
console.log(void 0); // undefined

????=

空值合并运算符(??

空值合并运算符(??)是一个逻辑运算符,判断左边的值是否是 nullundefined。如代码 a ?? b,如果 anullundefined,返回 b, 反之,返回 a

a ?? b 可以理解为: a !== undefined && a !== null ? a : b

我们可以给值为 nullundefined 的变量一个默认值。如下面的代码所示:

1
2
3
let firstName = null, lastName = 'Sun';
let fullName = firstName ?? lastName
console.log(fullName); // Sun

?? vs ||

??|| 的相同点:都可以为值为 nullundefined 的变量赋默认值。如下面代码:

1
2
3
4
5
6
let firstName = null, lastName = 'Sun';
let fullName = firstName ?? lastName
console.log(fullName); // Sun

fullName = firstName || lastName;
console.log(fullName); // Sun

不同点在于:

  • ?? 只判断值 nullundefined

  • || 是任何假值(0''NaNnullundefined)都不会被返回。这导致如果你使用 0''NaN 作为有效值,就会出现不可预料的后果。

如下面代码,判断值是 '' 时两者的不同表现:

1
2
3
4
5
6
let firstName = '', lastName = 'Sun';
let fullName = firstName ?? lastName
console.log(fullName); // ''

fullName = firstName || lastName;
console.log(fullName); // Sun

逻辑空赋值运算符 ??=

逻辑空赋值运算符(x ??= y)仅在 x 是空值(nullundefined)时对其赋值。

a ??= b 可以理解为: a ?? (a = b)

如下面的代码:

1
2
3
let firstName = null;
firstName ??= 'yangyang'
console.log(firstName); // 'yangyang'

【参考资料】
空值合并运算符(??) - JavaScript | MDN
逻辑空赋值(??=) - JavaScript | MDN

typeof null 的结果是什么?

面试高频指数:★★★★☆

JavaScript 中,typeof nullobject,这是不对的,因为 null 是基本数据类型,不是对象。这是个 bug,但是因为修复这个 bug 会影响现存的代码,所以就一直没改。

这个 bug 是 JavaScript 第一版的遗留物,这个版本中,值都是 32 位存储单元,由类型标签(1-3位)和实际的值组成。类型标签存在单元的低位里,有下面五种:

  • 000: 对象,数据是对象类型
  • 1:整数,存储的数据是一个 31 位的有符号整数。
  • 010:浮点数,存的数据是双精度浮点数
  • 100: 字符串,存的数据是字符串
  • 110:布尔,存的数据是布尔

低位如果是 1 位,类型标签就是 1 位长度(如整数类型),如果是 0,类型标签是 3 位长度,提供两个额外的位,如其余的四个类型。

小练习

typeof NaN 返回什么?

答案:typeof NaN 返回 'number'NaN 表示不是一个数字,它是 Number 的特殊值。

【参考资料】
The history of “typeof null”

2. js基础

2.1 let const var 相关

参考答案:

var ——ES5 变量声明方式

  1. 在变量未赋值时,变量undefined(为使用声明变量时也为undefined)
  2. 作用域——var的作用域为方法作用域;只要在方法内定义了,整个方法内的定义变量后的代码都可以使用

let——ES6变量声明方式

  1. 在变量为声明前直接使用会报错
  2. 作用域——let为块作用域——通常let比var 范围要小
  3. let禁止重复声明变量,否则会报错;var可以重复声明

const——ES6变量声明方式

\1. const为常量声明方式;声明变量时必须初始化,在后面出现的代码中不能再修改该常量的值

\2. const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动

2.2 js数据类型,区别

参考答案:

基本数据类型:

Number,String,Boolean,null,undefined,symbol,bigint(后两个为ES6新增)

引用数据类型:

object,function(proto Function.prototype)

object:普通对象,数组对象,正则对象,日期对象,Math数学函数对象。

两种数据存储方式:

基本数据类型是直接存储在栈中的简单数据段,占据空间小、大小固定,属于被频繁使用的数据。栈是存储基 本类型值和执行代码的空间。

引用数据类型是存储在堆内存中,占据空间大、大小不固定。引用数据类型在栈中存储了指针,该指针指向堆 中该实体的起始地址,当解释器寻找引用值时,会检索其在栈中的地址,取得地址后从堆中获得实体。

两种数据类型的区别:

  1. 堆比栈空间大,栈比堆运行速度快。
  2. 堆内存是无序存储,可以根据引用直接获取。
  3. 基础数据类型比较稳定,而且相对来说占用的内存小。
  4. 引用数据类型大小是动态的,而且是无限的。

2.3 Object.assign的理解

参考答案:

作用:Object.assign可以实现对象的合并。

语法:Object.assign(target, …sources)

解析

  1. Object.assign会将source里面的可枚举属性复制到target,如果和target的已有属性重名,则会覆盖。
  2. 后续的source会覆盖前面的source的同名属性。
  3. Object.assign复制的是属性值,如果属性值是一个引用类型,那么复制的其实是引用地址,就会存在引用共享的问题。

2.4 constructor的理解

参考答案:

创建的每个函数都有一个prototype(原型)对象,这个属性是一个指针,指向一个对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(继承自构造函数的prototype),指向构造函数的原型对象。注意当将构造函数的prototype设置为等于一个以对象字面量形式创建的新对象时,constructor属性不再指向该构造函数。

2.5 map 和 forEach 的区别

参考答案:

相同点:

  1. 都是循环遍历数组中的每一项
  2. 每次执行匿名函数都支持三个参数,参数分别为item(当前每一项),index(索引值),arr(原数组)
  3. 匿名函数中的this都是指向window
  4. 只能遍历数组

不同点:

  1. map()会分配内存空间存储新数组并返回,forEach()不会返回数据。
  2. forEach()允许callback更改原始数组的元素。map()返回新的数组。

2.6 for of 可以遍历哪些对象

参考答案:

for..of..: 它是es6新增的一个遍历方法,但只限于迭代器(iterator), 所以普通的对象用for..of遍历
是会报错的。

可迭代的对象:包括Array, Map, Set, String, TypedArray, arguments对象等等

2.7 js静态类型检查

参考答案:

js是动态类型语言

静态类型语言 & 动态类型语言

静态类型语言:类型检查发生在编译阶段,因此除非修复错误,否则会一直编译失败

动态类型语言:只有在程序运行了一次的时候错误才会被发现,也就是在运行时,因此即使代码中包含了会 在运行时阻止脚本正常运行的错误类型,这段代码也可以通过编译

js静态类型检查的方法

Flow是Facebook开发和发布的一个开源的静态类型检查库,它允许你逐渐地向JavaScript代码中添加类型。

TypeScript是一个会编译为JavaScript的超集(尽管它看起来几乎像一种新的静态类型语言)

使用静态类型的优势

  • 可以尽早发现bug和错误
  • 减少了复杂的错误处理
  • 将数据和行为分离
  • 减少单元测试的数量
  • 提供了领域建模(domain modeling)工具
  • 帮助我们消除了一整类bug
  • 重构时更有信心

使用静态类型的劣势

  • 代码冗长
  • 需要花时间去掌握类型

2.8 indexof

参考答案:

语法:str.indexOf(searchValue [, fromIndex])

参数:searchValue:要被查找的字符串值。

如果没有提供确切地提供字符串,[searchValue 会被强制设置为”undefined”], 然后在当前字符串中查 找这个值。

举个例子:’undefined’.indexOf()将会返回0,因为undefined在位置0处被找到,但是’undefine’.indexOf()将会返回 -1 ,因为字符串’undefined’未被找到

fromIndex:可选

数字表示开始查找的位置。可以是任意整数,默认值为0。

如果fromIndex的值小于0,或者大于str.length,那么查找分别从0和str.length开始。(译者 注:fromIndex的值小于0,等同于为空情况;fromIndex的值大于或等于str.length,那么结果 会直接返回-1。)

举个例子,’hello world’.indexOf(‘o’, -5)返回4,因为它是从位置0处开始查找,然后o在位置4处被找到。另一方面,’hello world’.indexOf(‘o’, 11)(或fromIndex填入任何大于11的值) 将会返回-1,因为开始查找的位置11处,已经是这个字符串的结尾了。

返回值:

查找的字符串searchValue的第一次出现的索引,如果没有找到,则返回-1。

若被查找的字符串searchValue是一个空字符串,则返回fromIndex。如果fromIndex值为空,或者fromIndex值小于被查找的字符串的长度,返回值和以下的fromIndex值一样。

如果fromIndex值大于等于字符串的长度,将会直接返回字符串的长度(str.length)

特点:

\1. 严格区分大小写

\2. 在使用indexOf检索数组时,用‘===’去匹配,意味着会检查数据类型

2.9 iframe有什么优点、缺点

参考答案:

优点:

  1. iframe能够原封不动的把嵌入的网页展现出来。
  2. 如果有多个网页引用iframe,那么你只需要修改iframe的内容,就可以实现调用的每一个页面内容的更改,方便快捷。
  3. 网页如果为了统一风格,头部和版本都是一样的,就可以写成一个页面,用iframe来嵌套,可以增加代码的可重用。
  4. 如果遇到加载缓慢的第三方内容如图标和广告,这些问题可以由iframe来解决。

缺点:

  1. iframe会阻塞主页面的onload事件;
  2. iframe和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载。会产生很多页面,不容易管理。
  3. iframe框架结构有时会让人感到迷惑,如果框架个数多的话,可能会出现上下、左右滚动条,会分散访问者的注意力,用户体验度差。
  4. 代码复杂,无法被一些搜索引擎索引到,这一点很关键,现在的搜索引擎爬虫还不能很好的处理iframe中的内容,所以使用iframe会不利于搜索引擎优化(SEO)。
  5. 很多的移动设备无法完全显示框架,设备兼容性差。
  6. iframe框架页面会增加服务器的http请求,对于大型网站是不可取的。

2.10 webComponents

参考答案:

Web Components 总的来说是提供一整套完善的封装机制来把 Web 组件化这个东西标准化,每个框架实现 的组件都统一标准地进行输入输出,这样可以更好推动组件的复用

包含四个部分

\1. Custom Elements

\2. HTML Imports

\3. HTML Templates

\4. Shadow DOM

Custom Elements

提供一种方式让开发者可以自定义 HTML 元素,包括特定的组成,样式和行为。支持 Web Components 标准的浏览器会提供一系列 API 给开发者用于创建自定义的元素,或者扩展现有元素。

HTML Imports

一种在 HTMLs 中引用以及复用其他的 HTML 文档的方式。这个 Import 很漂亮,可以简单理解为我们常见 的模板中的include之类的作用

HTML Templates

模板

Shadow DOM

提供一种更好地组织页面元素的方式,来为日趋复杂的页面应用提供强大支持,避免代码间的相互影响

2.11 dva的数据流流向是怎么样的

参考答案:

数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据 的时候可以通过dispatch发起一个 action,如果是同步行为会直接通过Reducers改变State,如果是 异步行为(副作用)会先触发Effects然后流向Reducers最终改变State,所以在 dva 中,数据流向非 常清晰简明,并且思路基本跟开源社区保持一致。

img

2.12 变量提升

参考答案:

JavaScript是单线程语言,所以执行肯定是按顺序执行。但是并不是逐行的分析和执行,而是一段一段地分析执行,会先进行编译阶段然后才是执行阶段。在编译阶段阶段,代码真正执行前的几毫秒,会检测到所有的变量和函数声明,所有这些函数和变量声明都被添加到名为Lexical Environment的JavaScript数据结构内的内存中。所以这些变量和函数能在它们真正被声明之前使用。

2.13 作用域

参考答案:

概念:作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6 的到来,为我们提供了‘块级作用域’,可通过新增命令 let 和 const 来体现。

扩展:

var ——ES5 变量声明方式

  1. 在变量未赋值时,变量undefined(为使用声明变量时也为undefined)
  2. 作用域——var的作用域为方法作用域;只要在方法内定义了,整个方法内的定义变量后的代码都可以使用

let——ES6变量声明方式

  1. 在变量为声明前直接使用会报错
  2. 作用域——let为块作用域——通常let比var 范围要小
  3. let禁止重复声明变量,否则会报错;var可以重复声明

const——ES6变量声明方式

const为常量声明方式;声明变量时必须初始化,在后面出现的代码中不能再修改该常量的值

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动

2.14 HashMap 和 Array 有什么区别?

参考答案:

  1. 查找效率
    HashMap因为其根据hashcode的值直接算出index,所以其查找效率是随着数组长度增大而增加的。
    ArrayMap使用的是二分法查找,所以当数组长度每增加一倍时,就需要多进行一次判断,效率下降
  2. 扩容数量
    HashMap初始值16个长度,每次扩容的时候,直接申请双倍的数组空间。
    ArrayMap每次扩容的时候,如果size长度大于8时申请size*1.5个长度,大于4小于8时申请8个,小于4时申 请4个。这样比较ArrayMap其实是申请了更少的内存空间,但是扩容的频率会更高。因此,如果数据量比较大的时候,还是使用HashMap更合适,因为其扩容的次数要比ArrayMap少很多。
  3. 扩容效率
    HashMap每次扩容的时候重新计算每个数组成员的位置,然后放到新的位置。
    ArrayMap则是直接使用System.arraycopy,所以效率上肯定是ArrayMap更占优势。
  4. 内存消耗
    以ArrayMap采用了一种独特的方式,能够重复的利用因为数据扩容而遗留下来的数组空间,方便下一个ArrayMap的使用。而HashMap没有这种设计。 由于ArrayMap之缓存了长度是4和8的时候,所以如果频繁的使用到Map,而且数据量都比较小的时候,ArrayMap无疑是相当的是节省内存的。

总结
综上所述,数据量比较小,并且需要频繁的使用Map存储数据的时候,推荐使用ArrayMap。 而数据量比较大的 时候,则推荐使用HashMap。

2.15 HashMap和Object

参考答案:

Objects和Maps类似的是,它们都允许你按键存取一个值、删除键、检测一个键是否绑定了值。因此(并且也没有其他内建的替代方式了)过去我们一直都把对象当成Maps使用。不过Maps和Objects有一些重要的区别,在下列情况里使用Map会是更好的选择:

Map Object
意外的键 Map默认情况不包含任何键。只包含显式插入的键。 一个Object有一个原型, 原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。注意: 虽然 ES5 开始可以用Object.create(null)来创建一个没有原型的对象,但是这种用法不太常见。
键的类型 一个Map的键可以是任意值,包括函数、对象或任意基本类型。 一个Object的键必须是一个 String 或是Symbol
键的顺序 Map中的 key 是有序的。因此,当迭代的时候,一个Map对象以插入的顺序返回键值。 一个Object的键是无序的注意:自ECMAScript 2015规范以来,对象确实保留了字符串和Symbol键的创建顺序; 因此,在只有字符串键的对象上进行迭代将按插入顺序产生键。
Size Map的键值对个数可以轻易地通过size 属性获取 Object的键值对个数只能手动计算
迭代 Map是 iterable 的,所以可以直接被迭代。 迭代一个Object需要以某种方式获取它的键然后才能迭代。
性能 在频繁增删键值对的场景下表现更好。 在频繁添加和删除键值对的场景下未作出优化。

2.16 javascript中arguments相关的问题

参考答案:

arguments

在js中,我们在调用有参数的函数时,当往这个调用的有参函数传参时,js会把所传的参数全部存到一个叫arguments的对象里面。它是一个类数组数据

由来

Javascrip中每个函数都会有一个Arguments对象实例arguments,引用着函数的实参。它是寄生在js函数当中的,不能显式创建,arguments对象只有函数开始时才可用

作用

有了arguments这个对象之后,我们可以不用给函数预先设定形参了,可以动态地通过arguments为函数加入参数

2.17 instanceOf 原理,手动实现 function isInstanceOf (child, Parent)

参考答案

instanceof主要作用就是判断一个实例是否属于某种类型

1
2
3
4
5
let person = function(){

}
let no = new person()
no instanceof person//true

instanceOf 原理

1
2
3
4
5
6
7
8
9
10
11
12
13
function new_instance_of(leftVaule, rightVaule) { 
let rightProto = rightVaule.prototype; // 取右表达式的 prototype 值
leftVaule = leftVaule.__proto__; // 取左表达式的__proto__值
while (true) {
if (leftVaule === null) {
return false;
}
if (leftVaule === rightProto) {
return true;
}
leftVaule = leftVaule.__proto__
}
}

其实 instanceof 主要的实现原理就是只要右边变量的 prototype 在左边变量的原型链上即可。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。

同时还要了解js的原型继承原理

img

我们知道每个 JavaScript 对象均有一个隐式的 proto 原型属性,而显式的原型属性是 prototype,只有 Object.prototype.proto 属性在未修改的情况下为 null 值

手动实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
var O = R.prototype;
L = L.__proto__;
while (true) {
if (L === null)
return false;
if (O === L) // 这里重点:当 O 严格等于 L 时,返回true
return true;
L = L.__proto__;
}
}
// 开始测试
var a = []
var b = {}

function Foo(){}
var c = new Foo()
function child(){}
function father(){}
child.prototype = new father()
var d = new child()

console.log(instance_of(a, Array)) // true
console.log(instance_of(b, Object)) // true
console.log(instance_of(b, Array)) // false
console.log(instance_of(a, Object)) // true
console.log(instance_of(c, Foo)) // true
console.log(instance_of(d, child)) // true
console.log(instance_of(d, father)) // true

2.18 数组去重

参考答案:

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

1
2
3
4
5
6
function unique (arr) {
return Array.from(new Set(arr))
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]

不考虑兼容性,这种去重的方法代码最少。这种方法还无法去掉“{}”空对象,后面的高阶方法会添加去掉重复“{}”的方法。

2. 利用for嵌套for,然后splice去重(ES5中最常用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", 15, false, undefined, NaN, NaN, "NaN", "a", {…}, {…}] //NaN和{}没有去重,两个null直接消失了

双层循环,外层循环元素,内层循环时比较值。值相同时,则删去这个值。

3. 利用indexOf去重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error!')
return
}
var array = [];
for (var i = 0; i < arr.length; i++) {
if (array .indexOf(arr[i]) === -1) {
array .push(arr[i])
}
}
return array;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
// [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}] //NaN、{}没有去重

新建一个空的结果数组,for 循环原数组,判断结果数组是否存在当前元素,如果有相同的值则跳过,不相同则push进数组。

4. 利用sort()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error!')
return;
}
arr = arr.sort()
var arrry= [arr[0]];
for (var i = 1; i < arr.length; i++) {
if (arr[i] !== arr[i-1]) {
arrry.push(arr[i]);
}
}
return arrry;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
// [0, 1, 15, "NaN", NaN, NaN, {…}, {…}, "a", false, null, true, "true", undefined] //NaN、{}没有去重

利用sort()排序方法,然后根据排序后的结果进行遍历及相邻元素比对。

5. 利用对象的属性不能相同的特点进行去重(这种数组去重的方法有问题,不建议用,有待改进)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", 15, false, undefined, null, NaN, 0, "a", {…}] //两个true直接去掉了,NaN和{}去重

6. 利用includes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error!')
return
}
var array =[];
for(var i = 0; i < arr.length; i++) {
if( !array.includes( arr[i]) ) {//includes 检测数组是否有某个值
array.push(arr[i]);
}
}
return array
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}] //{}没有去重

7. 利用hasOwnProperty

1
2
3
4
5
6
7
8
9
function unique(arr) {
var obj = {};
return arr.filter(function(item, index, arr){
return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
})
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}] //所有的都去重了

利用hasOwnProperty 判断是否存在对象属性

8. 利用filter

1
2
3
4
5
6
7
8
9
function unique(arr) {
return arr.filter(function(item, index, arr) {
//当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
return arr.indexOf(item, 0) === index;
});
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, "NaN", 0, "a", {…}, {…}]

9. 利用递归去重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function unique(arr) {
var array= arr;
var len = array.length;

array.sort(function(a,b){ //排序后更加方便去重
return a - b;
})

function loop(index){
if(index >= 1){
if(array[index] === array[index-1]){
array.splice(index,1);
}
loop(index - 1); //递归loop,然后数组去重
}
}
loop(len-1);
return array;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "a", "true", true, 15, false, 1, {…}, null, NaN, NaN, "NaN", 0, "a", {…}, undefined]

10. 利用Map数据结构去重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 ;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "a", "true", true, 15, false, 1, {…}, null, NaN, NaN, "NaN", 0, "a", {…}, undefined]

创建一个空Map数据结构,遍历需要去重的数组,把数组的每一个元素作为key存到Map中。由于Map中不会出现相同的key值,所以最终得到的就是去重后的结果。

11. 利用reduce+includes

1
2
3
4
5
6
function unique(arr){
return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]);
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr));
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]

12. […new Set(arr)]

1
2
[...new Set(arr)] 
//代码就是这么少----(其实,严格来说并不算是一种,相对于第一种方法来说只是简化了代码)

2.19 编码和字符集的区别

参考答案:

字符集是书写系统字母与符号的集合,而字符编码则是将字符映射为一特定的字节或字节序列,是一种规则。通常特定的字符集采用特定的编码方式(即一种字符集对应一种字符编码(例如:ASCII、IOS-8859-1、GB2312、GBK,都是即表示了字符集又表示了对应的字符编码,但Unicode不是,它采用现代的模型))

扩展:

字符:在计算机和电信技术中,一个字符是一个单位的字形、类字形单位或符号的基本信息。即一个字符可以是一个中文汉字、一个英文字母、一个阿拉伯数字、一个标点符号等。

字符集:多个字符的集合。例如GB2312是中国国家标准的简体中文字符集,GB2312收录简化汉字(6763个)及一般符号、序号、数字、拉丁字母、日文假名、希腊字母、俄文字母、汉语拼音符号、汉语注音字母,共 7445 个图形字符。

字符编码:把字符集中的字符编码为(映射)指定集合中的某一对象(例如:比特模式、自然数序列、电脉冲),以便文本在计算机中存储和通过通信网络的传递。

2.20 null 和 undefined 的区别,如何让一个属性变为null

参考答案:

undefined 表示一个变量自然的、最原始的状态值,而 null 则表示一个变量被人为的设置为空对象,而不是原始状态。所以,在实际使用过程中,为了保证变量所代表的语义,不要对一个变量显式的赋值 undefined,当需要释放一个对象时,直接赋值为 null 即可。

解析:

undefined 的字面意思就是:未定义的值 。这个值的语义是,希望表示一个变量最原始的状态,而非人为操作的结果 。 这种原始状态会在以下 4 种场景中出现:

  1. 声明了一个变量,但没有赋值
  2. 访问对象上不存在的属性
  3. 函数定义了形参,但没有传递实参
  4. 使用 void 对表达式求值

因此,undefined 一般都来自于某个表达式最原始的状态值,不是人为操作的结果。当然,你也可以手动给一个变量赋值 undefined,但这样做没有意义,因为一个变量不赋值就是 undefined 。

null 的字面意思是:空值 。这个值的语义是,希望表示 一个对象被人为的重置为空对象,而非一个变量最原始的状态 。 在内存里的表示就是,栈中的变量没有指向堆中的内存对象

img

null 有属于自己的类型 Null,而不属于Object类型,typeof 之所以会判定为 Object 类型,是因为JavaScript 数据类型在底层都是以二进制的形式表示的,二进制的前三位为 0 会被 typeof 判断为对象类型,而 null 的二进制位恰好都是 0 ,因此,null 被误判断为 Object 类型。

2.21 数组和伪数组的区别

参考答案

  1. 定义
  • 数组是一个特殊对象,与常规对象的区别:
    • 当由新元素添加到列表中时,自动更新length属性
    • 设置length属性,可以截断数组
    • 从Array.protoype中继承了方法
    • 属性为’Array’
  • 类数组是一个拥有length属性,并且他属性为非负整数的普通对象,类数组不能直接调用数组方法。
  1. 区别
    本质:类数组是简单对象,它的原型关系与数组不同。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 原型关系和原始值转换
let arrayLike = {
length: 10,
};
console.log(arrayLike instanceof Array); // false
console.log(arrayLike.__proto__.constructor === Array); // false
console.log(arrayLike.toString()); // [object Object]
console.log(arrayLike.valueOf()); // {length: 10}

let array = [];
console.log(array instanceof Array); // true
console.log(array.__proto__.constructor === Array); // true
console.log(array.toString()); // ''
console.log(array.valueOf()); // []
  1. 类数组转换为数组
  • 转换方法
    • 使用Array.from()
    • 使用Array.prototype.slice.call()
    • 使用Array.prototype.forEach()进行属性遍历并组成新的数组
  • 转换须知
    • 转换后的数组长度由length属性决定。索引不连续时转换结果是连续的,会自动补位。
    • 代码示例
1
2
3
4
5
6
7
8
9
let al1 = {
length: 4,
0: 0,
1: 1,
3: 3,
4: 4,
5: 5,
};
console.log(Array.from(al1)) // [0, 1, undefined, 3]
  • ②仅考虑 0或正整数 的索引
1
2
3
4
5
6
7
8
9
// 代码示例
let al2 = {
length: 4,
'-1': -1,
'0': 0,
a: 'a',
1: 1
};
console.log(Array.from(al2)); // [0, 1, undefined, undefined]
  • ③使用slice转换产生稀疏数组
1
2
3
4
5
6
7
8
9
// 代码示例
let al2 = {
length: 4,
'-1': -1,
'0': 0,
a: 'a',
1: 1
};
console.log(Array.prototype.slice.call(al2)); //[0, 1, empty × 2]
  1. 使用数组方法操作类数组注意地方
1
2
3
4
5
6
7
8
9
10
11
12
let arrayLike2 = {
2: 3,
3: 4,
length: 2,
push: Array.prototype.push
}

// push 操作的是索引值为 length 的位置
arrayLike2.push(1);
console.log(arrayLike2); // {2: 1, 3: 4, length: 3, push: ƒ}
arrayLike2.push(2);
console.log(arrayLike2); // {2: 1, 3: 2, length: 4, push: ƒ}

2.22 手写一个发布订阅

参考答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 发布订阅中心, on-订阅, off取消订阅, emit发布, 内部需要一个单独事件中心caches进行存储;
interface CacheProps {
[key: string]: Array<((data?: unknown) => void)>;
}

class Observer {
private caches: CacheProps = {}; // 事件中心
on (eventName: string, fn: (data?: unknown) => void){ // eventName事件名-独一无二, fn订阅后执行的自定义行为
this.caches[eventName] = this.caches[eventName] || [];
this.caches[eventName].push(fn);
}

emit (eventName: string, data?: unknown) { // 发布 => 将订阅的事件进行统一执行
if (this.caches[eventName]) {
this.caches[eventName].forEach((fn: (data?: unknown) => void) => fn(data));
}
}

off (eventName: string, fn?: (data?: unknown) => void) { // 取消订阅 => 若fn不传, 直接取消该事件所有订阅信息
if (this.caches[eventName]) {
const newCaches = fn ? this.caches[eventName].filter(e => e !== fn) : [];
this.caches[eventName] = newCaches;
}
}

}

2.23 手写数组转树

参考答案

问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 例如将 input 转成output的形式
let input = [
{
id: 1, val: '学校', parentId: null
}, {
id: 2, val: '班级1', parentId: 1
}, {
id: 3, val: '班级2', parentId: 1
}, {
id: 4, val: '学生1', parentId: 2
}, {
id: 5, val: '学生2', parentId: 2
}, {
id: 6, val: '学生3', parentId: 3
},
]

let output = {
id: 1,
val: '学校',
children: [{
id: 2,
val: '班级1',
children: [
{
id: 4,
val: '学生1',
children: []
},
{
id: 5,
val: '学生2',
children: []
}
]
}, {
id: 3,
val: '班级2',
children: [{
id: 6,
val: '学生3',
children: []
}]
}]
}

答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 代码实现
function arrayToTree(array) {
let root = array[0]
array.shift()
let tree = {
id: root.id,
val: root.val,
children: array.length > 0 ? toTree(root.id, array) : []
}
return tree;
}

function toTree(parenId, array) {
let children = []
let len = array.length
for (let i = 0; i < len; i++) {
let node = array[i]
if (node.parentId === parenId) {
children.push({
id: node.id,
val: node.val,
children: toTree(node.id, array)
})
}
}
return children
}

console.log(arrayToTree(input))

2.24 介绍下 Set、Map、WeakSet 和 WeakMap 的区别?

参考答案

Set

  1. 成员不能重复;
  2. 只有键值,没有键名,有点类似数组;
  3. 可以遍历,方法有add、delete、has

WeakSet

  1. 成员都是对象(引用);
  2. 成员都是弱引用,随时可以消失(不计入垃圾回收机制)。可以用来保存 DOM 节点,不容易造成内存泄露;
  3. 不能遍历,方法有add、delete、has;

Map

  1. 本质上是键值对的集合,类似集合;
  2. 可以遍历,方法很多,可以跟各种数据格式转换;

WeakMap

  1. 只接收对象为键名(null 除外),不接受其他类型的值作为键名;
  2. 键名指向的对象,不计入垃圾回收机制;
  3. 不能遍历,方法同get、set、has、delete;

2.25 简单说说 js 中有哪几种内存泄露的情况

参考答案

  1. 意外的全局变量;
  2. 闭包;
  3. 未被清空的定时器;
  4. 未被销毁的事件监听;
  5. DOM 引用;

2.26 异步笔试题

请写出下面代码的运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 今日头条面试题

async function async1() {

console.log('async1 start')

await async2()

console.log('async1 end')

}

async function async2() {

console.log('async2')

}

console.log('script start')

setTimeout(function () {

console.log('settimeout')

})

async1()

new Promise(function (resolve) {

console.log('promise1')

resolve()

}).then(function () {

console.log('promise2')

})

console.log('script end')

题目的本质,就是考察setTimeout、promise、async await的实现及执行顺序,以及 JS 的事件循环的相关问题。

答案:

1
2
3
4
5
6
7
8
script start
async1 start
async2
promise1
script end
async1 end
promise2
settimeout

2.27 json和xml数据的区别

参考答案

  1. 数据体积方面:xml是重量级的,json是轻量级的,传递的速度更快些。
  2. 数据传输方面:xml在传输过程中比较占带宽,json占带宽少,易于压缩。
  3. 数据交互方面:json与javascript的交互更加方便,更容易解析处理,更好的进行数据交互
  4. 数据描述方面:json对数据的描述性比xml较差
  5. xml和json都用在项目交互下,xml多用于做配置文件,json用于数据交互。

2.28 JavaScript有几种方法判断变量的类型?

参考答案

  1. 使用typeof检测当需要判断变量是否是number, string, boolean, function, undefined等类型时,可以使用typeof进行判断。
  2. 使用instanceof检测instanceof运算符与typeof运算符相似,用于识别正在处理的对象的类型。与typeof方法不同的是,instanceof 方法要求开发者明确地确认对象为某特定类型。
  3. 使用constructor检测constructor本来是原型对象上的属性,指向构造函数。但是根据实例对象寻找属性的顺序,若实例对象上没有实例属性或方法时,就去原型链上寻找,因此,实例对象也是能使用constructor属性的。

2.29 代码解释题

参考答案

题目:

1
2
3
4
var min = Math.min();
max = Math.max();
console.log(min < max);
// 写出执行结果,并解释原因

答案
false

解析

  • 按常规的思路,这段代码应该输出 true,毕竟最小值小于最大值。但是却输出 false
  • MDN 相关文档是这样解释的
    • Math.min 的参数是 0 个或者多个,如果多个参数很容易理解,返回参数中最小的。如果没有参数,则返回 Infinity,无穷大。
    • 而 Math.max 没有传递参数时返回的是-Infinity.所以输出 false

2.30 代码解析题

参考答案

题目

1
2
3
4
5
6
7
var company = {
address: 'beijing'
}
var yideng = Object.create(company);
delete yideng.address
console.log(yideng.address);
// 写出执行结果,并解释原因

答案
beijing

解析
这里的 yideng 通过 prototype 继承了 company的 address。yideng自己并没有address属性。所以delete操作符的作用是无效的。

扩展
1.delete使用原则:delete 操作符用来删除一个对象的属性。
2.delete在删除一个不可配置的属性时在严格模式和非严格模式下的区别:
(1)在严格模式中,如果属性是一个不可配置(non-configurable)属性,删除时会抛出异常;
(2)非严格模式下返回 false。
3.delete能删除隐式声明的全局变量:这个全局变量其实是global对象(window)的属性
4.delete能删除的:
(1)可配置对象的属性(2)隐式声明的全局变量 (3)用户定义的属性 (4)在ECMAScript 6中,通过 const 或 let 声明指定的 “temporal dead zone” (TDZ) 对 delete 操作符也会起作用
delete不能删除的:
(2)显式声明的全局变量 (2)内置对象的内置属性 (3)一个对象从原型继承而来的属性
5.delete删除数组元素:
(1)当你删除一个数组元素时,数组的 length 属性并不会变小,数组元素变成undefined
(2)当用 delete 操作符删除一个数组元素时,被删除的元素已经完全不属于该数组。
(3)如果你想让一个数组元素的值变为 undefined 而不是删除它,可以使用 undefined 给其赋值而不是使用 delete 操作符。此时数组元素是在数组中的
6.delete 操作符与直接释放内存(只能通过解除引用来间接释放)没有关系。