Skip to content

前端基础 Javascript

声明

整个知识库涉及的资料一部分为自主收集整理,一部分来源于网络收集,如有侵权请联系我

JavaScript有哪些数据类型,它们的区别?

JavaScript共有八种数据类型,分别是 UndefinedNullBooleanNumberStringObjectSymbolBigInt

其中 Symbol 和 BigInt 是ES6 中新增的数据类型:

  • Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
  • BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

这些数据可以分为原始数据类型引用数据类型

  • 栈:原始数据类型(undefined、null、boolean、number、string)
  • 堆:引用数据类型(对象、数组和函数)

两种类型的区别在于存储位置的不同

  • 原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
  • 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

堆和栈的概念存在于数据结构和操作系统内存中,在数据结构中:

  • 在数据结构中,栈中数据的存取方式为先进后出。
  • 堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。

在操作系统中,内存被分为栈区堆区

  • 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  • 堆区内存一般由开发着分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收。

为什么会有BigInt的提案?

JavaScript中Number.MAX_SAFE_INTEGER 表示最⼤安全数字,计算结果是 9007199254740991,即在这个数范围内不会出现精度丢失(⼩数除外)。但是⼀旦超过这个范围,js就会出现计算不准确的情况,这在⼤数计算的时候不得不依靠⼀些第三⽅库进⾏解决,因此官⽅提出了BigInt来解决此问题。

数据类型检测的方式有哪些

  1. typeof
javascript
console.log(typeof 2);               // number
console.log(typeof true);            // boolean
console.log(typeof 'str');           // string
console.log(typeof []);              // object    
console.log(typeof function(){});    // function
console.log(typeof {});              // object
console.log(typeof undefined);       // undefined
console.log(typeof null);            // object

其中数组、对象、null都会被判断为object,其他判断都正确

  1. instanceof

instanceof 可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型

javascript
console.log(2 instanceof Number);                    // false
console.log(true instanceof Boolean);                // false 
console.log('str' instanceof String);                // false 
 
console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true
console.log({} instanceof Object);                   // true

可以看到,instanceof 只能正确判断引用数据类型,而不能判断基本数据类型。instanceof 运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

  1. constructor
javascript
console.log((2).constructor === Number);                 // true
console.log((true).constructor === Boolean);             // true
console.log(('str').constructor === String);             // true
console.log(([]).constructor === Array);                 // true
console.log((function() {}).constructor === Function);   // true
console.log(({}).constructor === Object);                // true

constructor 有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor 对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了

javascript
function Fn(){};
 
Fn.prototype = new Array();
 
var f = new Fn();
 
console.log(f.constructor === Fn);    // false
console.log(f.constructor === Array); // true
  1. Object.prototype.toString.call()

Object.prototype.toString.call() 使用 Object 对象的原型方法 toString 来判断数据类型

javascript
console.log(Object.prototype.toString.call(2));             // [object Number]
console.log(Object.prototype.toString.call(true));          // [object Boolean]
console.log(Object.prototype.toString.call('str'));         // [object String]
console.log(Object.prototype.toString.call([]));            // [object Array]
console.log(Object.prototype.toString.call(function(){}));  // [object Function]
console.log(Object.prototype.toString.call({}));            // [object Object]
console.log(Object.prototype.toString.call(undefined));     // [object Undefined]
console.log(Object.prototype.toString.call(null));          // [object Null]

同样是检测对象 obj 调用 toString 方法,obj.toString() 的结果和 Object.prototype.toString.call(obj) 的结果不一样,这是为什么?

这是因为 toString 是 Object 的原型方法,而 Array、function 等类型作为 Object 的实例,都重写了 toString 方法。不同的对象类型调用toString 方法时,根据原型链的知识,调用的是对应的重写之后的 toString 方法(function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…),而不会去调用 Object 上原型 toString 方法(返回对象的具体类型),所以采用 obj.toString() 不能得到其对象类型,只能将obj 转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用 Object 原型上的 toString 方法。

判断数组的方式有哪些

  • 通过 Object.prototype.toString.call() 做判断
javascript
Object.prototype.toString.call(arr) === '[object Array]';
  • 通过原型链做判断
javascript
arr.__proto__ === Array.prototype;
  • 通过ES6的Array.isArray()做判断
javascript
Array.isArrray(arr);
  • 通过 instanceof 做判断
javascript
arr instanceof Array
  • 通过 Array.prototype.isPrototypeOf
javascript
Array.prototype.isPrototypeOf(arr)

null 和 undefined 区别

首先 undefinednull 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。

undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化。

undefined 在 JavaScript 中不是一个保留字,这意味着可以使用 undefined 来作为一个变量名,但是这样的做法是非常危险的,它会影响对 undefined 值的判断。我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。

当对这两种类型使用 typeof 进行判断时,null 类型化会返回 “object”,这是一个历史遗留的问题。当使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。

typeof null 的结果是什么,为什么?

typeof null 的结果是Object。

在 JavaScript 第一个版本中,所有值都存储在 32 位的单元中,每个单元包含一个小的 类型标签(1-3 bits) 以及当前要存储值的真实数据。类型标签存储在每个单元的低位中,共有五种数据类型:

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。

intanceof 操作符的实现原理及实现

instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。

javascript
function myInstanceof(left, right) {
  // 获取对象的原型
  let proto = Object.getPrototypeOf(left)
  // 获取构造函数的 prototype 对象
  let prototype = right.prototype; 
 
  // 判断构造函数的 prototype 对象是否在对象的原型链上
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;
    // 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
    proto = Object.getPrototypeOf(proto);
  }
}

Map 和 Object 的区别

MapObject
Map 默认情况不包含任何键,只包含显式插入的键Object 有一个原型, 原型链上的键名有可能和自己在对象上的设置的键名产生冲突
键类型Map的键可以是任意值,包括函数、对象或任意基本类型Object 的键必须是 String 或是Symbol。如果使用其他类型,会被自动转换为字符串
键的顺序Map 中的 key 是有序的。因此,当迭代的时候, Map 对象以插入的顺序返回键值Object 的键是无序的
大小Map 的键值对个数可以轻易地通过size 属性获取Object 的键值对个数只能手动计算
迭代Map 是 iterable 的,所以可以直接被迭代迭代 Object 需要以某种方式获取它的键然后才能迭代
性能在频繁增删键值对的场景下表现更好在频繁添加和删除键值对的场景下未作出优化

Map 和 WeakMap 的区别

MapWeakMap
键类型Map 的键可以是任何类型,包括对象、原始值(如字符串、数字等)eakMap 的键必须是对象,不能是原始值
键的引用Map 对键的引用是强引用,这意味着只要 Map 中存在键值对,键就不会被垃圾回收WeakMap 对键的引用是弱引用,这意味着如果没有其他引用指向键对象,键对象可以被垃圾回收
迭代Map 是可迭代的,可以使用 for...of 循环或 forEach 方法遍历其键值对WeakMap 不是可迭代的
大小Map 有一个 size 属性,可以直接获取其包含的键值对数量WeakMap 没有 size 属性,也没有任何方法可以获取其包含的键值对数量

WeakMap的设计目的在于,有时想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。一旦不再需要这两个对象,就必须手动删除这个引用,否则垃圾回收机制就不会释放对象占用的内存。

JavaScript 类数组对象的定义

一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。常见的类数组对象有 argumentsDOM 方法的返回结果,还有一个函数也可以被看作是类数组对象,因为它含有 length 属性值,代表可接收的参数个数。

常见的类数组转换为数组的方法有这样几种:

  1. 通过 call 调用数组的 slice 方法来实现转换
javascript
Array.prototype.slice.call(arrayLike);
  1. 通过 call 调用数组的 splice 方法来实现转换
javascript
Array.prototype.splice.call(arrayLike, 0);
  1. 通过 apply 调用数组的 concat 方法来实现转换
javascript
Array.prototype.concat.apply([], arrayLike);
  1. 通过 Array.from 方法来实现转换
javascript
Array.from(arrayLike);

为什么函数的 arguments 参数是类数组而不是数组?如何遍历类数组?

arguments 是一个对象,它的属性是从 0 开始依次递增的数字,还有 calleelength 属性,这和数组很像。但是它却不能调用数组的方法,这是因为:

  1. arguments 是一个对象,它的原型链中没有 Array.prototype,所以无法调用数组的方法
  2. arguments 对象的长度是根据实参的长度确定的,不是定义时参数的长度确定的
  3. arguments 对象的 callee 属性,指向当前执行的函数,这个属性在数组上是没有的

遍历类数组对象的方法有以下几种:

  1. for 循环
javascript
const arrayLike = {0: 'a', 1: 'b', 2: 'c', length: 3};
for (let i = 0; i < arrayLike.length; i++) {
  console.log(arrayLike[i]);
}
  1. Array.from() 转换为数组后遍历
javascript
const arrayLike = {0: 'a', 1: 'b', 2: 'c', length: 3};
// 转换为真正的数组
const arr = Array.from(arrayLike);
arr.forEach(item => console.log(item));
  1. 使用展开运算符将类数组转化成数组
javascript
const arrayLike = {0: 'a', 1: 'b', 2: 'c', length: 3};
const arr = [...arrayLike];
arr.forEach(item => console.log(item));
  1. 将数组的方法应用到类数组上,这时候就可以使用 callapply 方法
javascript
Array.prototype.forEach.call(arguments, a => console.log(a))

为什么0.1+0.2 ! == 0.3,如何让其相等

在开发过程中遇到类似这样的问题:

javascript
let n1 = 0.1
let n2 = 0.2
console.log(n1 + n2)  // 0.30000000000000004

计算机是通过二进制的方式存储数据的,所以计算机计算 0.1 + 0.2 的时候,实际上是计算的两个数的二进制的和。0.1 的二进制是 0.0001100110011001100...(1100循环),0.2的二进制是:0.00110011001100...(1100循环),这两个数的二进制都是无限循环的数。那JavaScript 是如何处理无限循环的二进制小数呢?

一般我们认为数字包括整数和小数,但是在 JavaScript 中只有一种数字类型:Number,它的实现遵循 IEEE 754 标准,使用 64位 固定长度来表示,也就是标准的 double 双精度浮点数。在二进制科学表示法中,双精度浮点数的小数部分最多只能保留 52位,再加上前面的 1,其实就是保留 53位 有效数字,剩余的需要舍去,遵从“0舍1入”的原则。

根据这个原则,0.1 和 0.2 的二进制数相加,再转化为十进制数就是:0.30000000000000004

下面看一下双精度数是如何保存的: IEEE 754

  • 第一部分(蓝色):用来存储符号位(sign),用来区分正负数,0 表示正数,占用 1位
  • 第二部分(绿色):用来存储指数(exponent),占用 11位
  • 第三部分(红色):用来存储小数(fraction),占用 52位

对于0.1,它的二进制为:

javascript
0.00011001100110011001100110011001100110011001100110011001 10011...

转为科学计数法(科学计数法的结果就是浮点数):

javascript
1.1001100110011001100110011001100110011001100110011001*2^-4

可以看出0.1的符号位为0,指数位为-4,小数位为:

javascript
1001100110011001100110011001100110011001100110011001

那么问题又来了,指数位是负数,该如何保存呢?

IEEE标准规定了一个偏移量,对于指数部分,每次都加这个偏移量进行保存,这样即使指数是负数,那么加上这个偏移量也就是正数了。由于JavaScript的数字是双精度数,这里就以双精度数为例,它的指数部分为 11位,能表示的范围就是 0~2047,IEEE固定双精度数的偏移量为 1023。

  • 当指数位不全是0也不全是1时(规格化的数值),IEEE规定,阶码计算公式为 e-Bias。 此时e最小值是1,则1-1023= -1022,e最大值是2046,则2046-1023=1023,可以看到,这种情况下取值范围是-1022~1013。
  • 当指数位全部是0的时候(非规格化的数值),IEEE规定,阶码的计算公式为1-Bias,即1-1023= -1022。
  • 当指数位全部是1的时候(特殊值),IEEE规定这个浮点数可用来表示3个特殊值,分别是正无穷,负无穷,NaN。 具体的,小数位不为0的时候表示NaN;小数位为0时,当符号位s=0时表示正无穷,s=1时候表示负无穷。

对于上面的0.1的指数位为-4,-4+1023 = 1019 转化为二进制就是:1111111011.

所以,0.1表示为:

javascript
0 1111111011 1001100110011001100110011001100110011001100110011001

说了这么多,是时候该最开始的问题了,如何实现 0.1+0.2=0.3 呢?

对于这个问题,一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对JavaScript来说,这个值通常为2-52,在ES6中,提供了Number.EPSILON 属性,而它的值就是2-52,只要判断0.1+0.2-0.3是否小于Number.EPSILON,如果小于,就可以判断为0.1+0.2===0.3

javascript
function numberepsilon(arg1,arg2){                   
  return Math.abs(arg1 - arg2) < Number.EPSILON;        
}        

console.log(numberepsilon(0.1 + 0.2, 0.3)); // true

typeof NaN 的结果是什么?

NaN 指 “不是一个数字”(not a number),NaN 是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。

javascript
typeof NaN; // "number"

NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN !== NaN 为 true。

isNaN 和 Number.isNaN 函数的区别?

  • 函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。

  • 函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。

其他值到字符串的转换规则?

  • Null 和 Undefined 类型 ,null 转换为 "null",undefined 转换为 "undefined",
  • Boolean 类型,true 转换为 "true",false 转换为 "false"。
  • Number 类型的值直接转换,不过那些极小和极大的数字会使用指数形式。
  • Symbol 类型的值直接转换,但是只允许显式强制类型转换,使用隐式强制类型转换会产生错误。
  • 对普通对象来说,除非自行定义 toString() 方法,否则会调用 toString()(Object.prototype.toString())来返回内部属性 [[Class]] 的值,如"[object Object]"。如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值。

其他值到数字值的转换规则?

  • Undefined 类型的值转换为 NaN。
  • Null 类型的值转换为 0。
  • Boolean 类型的值,true 转换为 1,false 转换为 0。
  • String 类型的值转换如同使用 Number() 函数进行转换,如果包含非数字值则转换为 NaN,空字符串为 0。
  • Symbol 类型的值不能转换为数字,会报错。
  • 对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。

为了将值转换为相应的基本类型值,抽象操作 ToPrimitive 会首先(通过内部操作 DefaultValue)检查该值是否有valueOf()方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。

如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。

其他值到布尔类型的值的转换规则?

以下这些是假值:

  • undefined
  • null
  • false
  • +0-0NaN
  • ""

假值的布尔强制类型转换结果为 false。从逻辑上说,假值列表以外的都应该是真值。

常见的位运算符

现代计算机中数据都是以二进制的形式存储的,即0、1两种状态,计算机对二进制数据进行的运算加减乘除等都是叫位运算,即将符号位共同参与运算的运算。

运算符描述运算规则
&两个位都为1时,结果才为1
|两个位都为0时,结果才为0
^异或两个位相同为0,相异为1
~取反0变1,1变0
<<左移各二进制位全部左移若干位,高位丢弃,低位补0
>>右移各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃
  1. 按位与运算符(&)

定义: 参加运算的两个数据按二进制位进行“与”运算。

运算规则

JavaScript
0 & 0 = 0  
0 & 1 = 0  
1 & 0 = 0  
1 & 1 = 1

总结:两位同时为1,结果才为1,否则结果为0。

例如:3&5 即:

JavaScript
0000 0011 
   0000 0101 
 = 0000 0001

因此 3&5 的值为1。 注意:负数按补码形式参加按位与运算。

用途

(1)判断奇偶 只要根据最未位是0还是1来决定,为0就是偶数,为1就是奇数。因此可以用if ((i & 1) == 0)代替if (i % 2 == 0)来判断a是不是偶数。

(2)清零 如果想将一个单元清零,即使其全部二进制位为0,只要与一个各位都为零的数值相与,结果为零。

  1. 按位或运算符(|)

定义: 参加运算的两个对象按二进制位进行“或”运算。

运算规则

JavaScript
0 | 0 = 0
0 | 1 = 1  
1 | 0 = 1  
1 | 1 = 1

总结:参加运算的两个对象只要有一个为1,其值为1。

例如:3|5即:

javascript
0000 0011
  0000 0101 
= 0000 0111

因此,3|5的值为7。 注意:负数按补码形式参加按位或运算。

  1. 异或运算符(^)

定义: 参加运算的两个数据按二进制位进行“异或”运算。

运算规则

javascript
0 ^ 0 = 0  
0 ^ 1 = 1  
1 ^ 0 = 1  
1 ^ 1 = 0

总结:参加运算的两个对象,如果两个相应位相同为0,相异为1。

例如:3|5即:

javascript
0000 0011
  0000 0101 
= 0000 0110

因此,3^5的值为6。

异或运算的性质:

  • 交换律:(a^b)^c == a^(b^c)
  • 结合律:(a + b)^c == a^b + b^c
  • 对于任何数x,都有 x^x=0,x^0=x
  • 自反性: a^b^b=a^0=a;
  1. 取反运算符 (~)

定义: 参加运算的一个数据按二进制进行“取反”运算。

运算规则

javascript
~ 1 = 0
~ 0 = 1

总结:对一个二进制数按位取反,即将0变1,1变0。

例如:~6 即:

javascript
0000 0110
= 1111 1001

在计算机中,正数用原码表示,负数使用补码存储,首先看最高位,最高位1表示负数,0表示正数。此计算机二进制码为负数,最高位为符号位。

当发现按位取反为负数时,就直接取其补码,变为十进制:

javascript
0000 0110
   = 1111 1001
反码:1000 0110
补码:1000 0111

因此,~6的值为-7。

  1. 左移运算符(<<)

定义: 将一个运算对象的各二进制位全部左移若干位,左边的二进制位丢弃,右边补0。

设 a=1010 1110,a = a<< 2 将a的二进制位左移2位、右补0,即得a=1011 1000。

若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2。

  1. 右移运算符(>>)

定义: 将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。

例如:a=a>>2 将a的二进制位右移2位,左补0 或者 左补1得看被移数是正还是负。

操作数每右移一位,相当于该数除以2。

  1. 原码、补码、反码

上面提到了补码、反码等知识,这里就补充一下。

计算机中的有符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位,三种表示方法各不相同。

(1)原码

原码就是一个数的二进制数。

例如:10的原码为0000 1010

(2)反码

正数的反码与原码相同,如:10 反码为 0000 1010

负数的反码为除符号位,按位取反,即0变1,1变0。

例如:-10

javacript
原码:1000 1010
反码:1111 0101

(3)补码

正数的补码与原码相同,如:10 补码为 0000 1010

负数的补码是原码除符号位外的所有位取反即0变1,1变0,然后加1,也就是反码加1。

例如:-10

javascript
原码:1000 1010
反码:1111 0101
补码:1111 0110

注意事项

  1. 优先级: 位运算符的优先级通常低于算术运算符
  2. 符号位: 右移时要注意符号位的处理
  3. 可读性: 虽然位运算效率高,但可能影响代码可读性

|| 和 && 操作符的返回值?

||&& 首先会对第一个操作数执行条件判断,如果其不是布尔值就先强制转换为布尔类型,然后再执行条件判断。

  • 对于 || 来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。
  • && 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。

|| 和 && 返回它们其中一个操作数的值,而非条件判断的结果

Object.is() 与比较操作符 “===”、“==” 的区别?

  • 使用双等号(==)进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。
  • 使用三等号(===)进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。
  • 使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的。

什么是 JavaScript 中的包装类型?

在 JavaScript 中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时 JavaScript 会在后台隐式地将基本类型的值转换为对象,如:

javascript
const a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"

在访问 'abc'.length 时,JavaScript 将 'abc' 在后台转换成 String('abc'),然后再访问其 length 属性。

JavaScript也可以使用 Object 函数显式地将基本类型转换为包装类型:

javascript
var a = 'abc'
Object(a) // String {"abc"}

也可以使用 valueOf 方法将包装类型倒转成基本类型

javascript
var a = 'abc'
var b = Object(a)
var c = b.valueOf() // 'abc'

看看如下代码会打印出什么:

javascript
var a = new Boolean( false );
if (!a) {
	console.log( "Oops" ); // never runs
}

答案是什么都不会打印,因为虽然包裹的基本类型是false,但是false被包裹成包装类型后就成了对象,所以其非值为false,所以循环体中的内容不会运行。

JavaScript 中如何进行隐式类型转换?

首先要介绍 ToPrimitive 方法,这是 JavaScript 中每个值隐含的自带的方法,用来将值 (无论是基本类型值还是对象)转换为基本类型值。如果值为基本类型,则直接返回值本身;如果值为对象,其看起来大概是这样:

javascript
/**
* @obj 需要转换的对象
* @type 期望的结果类型
*/
ToPrimitive(obj, type)

type的值为 number 或者 string

(1)当 type 为 number 时规则如下:

  • 调用 obj 的 valueOf 方法,如果为原始值,则返回,否则下一步;
  • 调用 obj的 toString 方法,后续同上;
  • 抛出 TypeError 异常

(2)当 type 为 string 时规则如下:

  • 调用 obj 的 toString 方法,如果为原始值,则返回,否则下一步;
  • 调用 obj 的 valueOf 方法,后续同上;
  • 抛出 TypeError 异常

可以看出两者的主要区别在于调用toString和valueOf的先后顺序。默认情况下:

  • 如果对象为 Date 对象,则type默认为string;
  • 其他情况下,type默认为number。

总结上面的规则,对于 Date 以外的对象,转换为基本类型的大概规则可以概括为一个函数:

javascript
var objToNumber = value => Number(value.valueOf().toString())
objToNumber([]) === 0
objToNumber({}) === NaN

而 JavaScript 中的隐式类型转换主要发生在 +-*/ 以及 ==>< 这些运算符之间。而这些运算符只能操作基本类型值,所以在进行这些运算前的第一步就是将两边的值用 ToPrimitive 转换成基本类型,再进行操作。

以下是基本类型的值在不同操作符的情况下隐式转换的规则 (对于对象,其会被ToPrimitive转换成基本类型,所以最终还是要应用基本类型转换规则):

  1. +操作符,+ 操作符的两边有至少一个 string 类型变量时,两边的变量都会被隐式转换为字符串;其他情况下两边的变量都会被转换为数字。
javascript
1 + '23' // '123'
1 + false // 1 
1 + Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number
'1' + false // '1false'
false + true // 1
  1. -*\ 操作符,NaN也是一个数字
javascript
1 * '23' // 23
1 * false // 0
1 / 'aa' // NaN
  1. == 操作符

操作符两边的值都尽量转成 number

javascript
3 == true // false, 3 转为number为3,true转为number为1
'0' == false //true, '0'转为number为0,false转为number为0
'0' == 0 // '0'转为number为0
  1. <> 比较符

如果两边都是字符串,则比较字母表顺序:

javascript
'ca' < 'bd' // false
'a' < 'b' // true

其他情况下,转换为数字再比较:

javascript
'12' < 13 // true
false > -1 // true

以上说的是基本类型的隐式转换,而对象会被 ToPrimitive 转换为基本类型再进行转换:

javascript
var a = {}
a > 2 // false

其对比过程如下:

javascript
a.valueOf() // {}, 上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步
a.toString() // "[object Object]",现在是一个字符串了
Number(a.toString()) // NaN,根据上面 < 和 > 操作符的规则,要转换成数字
NaN > 2 //false,得出比较结果

又比如:

javascript
var a = {name:'Jack'}
var b = {age: 18}
a + b // "[object Object][object Object]"

运算过程如下:

javascript
a.valueOf() // {},上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步
a.toString() // "[object Object]"
b.valueOf() // 同理
b.toString() // "[object Object]"
a + b // "[object Object][object Object]"

深拷贝和浅拷贝

  • 浅拷贝(Shallow Copy)

浅拷贝只复制对象的第一层属性,对于嵌套的对象,只复制其引用地址,而不是创建新的对象。所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

浅拷贝的实现方式:

  1. Object.assign()
  2. 扩展运算符(...)
  3. 数组的浅拷贝方法: slice()concat()Array.from()
  • 深拷贝(Deep Copy)

深拷贝会递归地复制对象的所有层级,创建一个完全独立的对象副本,包括所有嵌套的对象。修改新对象不会影响原对象。

深拷贝的实现方式:

  1. JSON方法(最简单但有限制)
  • 无法复制函数
  • 无法复制undefined
  • 无法复制Symbol
  • 无法处理循环引用
  • 会丢失Date对象的时间信息
  1. 手写递归实现深拷贝

  2. 使用第三方库如: lodash 的 cloneDeep;jQuery 的 extend 等

object.assign和扩展运算法是深拷贝还是浅拷贝

两者都是浅拷贝

  • Object.assign()方法接收的第一个参数作为目标对象,后面的所有参数作为源对象。然后把所有的源对象合并到目标对象中。它会修改了一个对象,因此会触发 ES6 setter。
  • 扩展操作符(…) 使用它时,数组或对象中的每一个值都会被拷贝到一个新的数组或对象中。它不复制继承的属性或类的属性,但是它会复制ES6的 symbols 属性。

如何判断一个对象是空对象

  • 使用JSON自带的.stringify方法来判断
javascript
if (Json.stringify(Obj) == '{}' ) {
  console.log('空对象');
}
  • 使用 ES6 新增的方法 Object.keys() 来判断
javascript
if (Object.keys(Obj).length < 0) {
  console.log('空对象');
}

对块级作用域的理解

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期

在ES6之前,ES的作用域只有两种:全局作用域函数作用域

  • 全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。

  • 函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。

在ES6之前,JavaScript只支持这两种作用域,相较而言,其他语言则都普遍支持块级作用域。

块级作用域(block scope) 指的是在一对大括号 {} 内定义的变量,其作用范围仅限于这个代码块内部,在代码块外部是不可访问的。简单来说,就是 {} 内的变量,在 {} 外是不可见的,也无法访问。

这种作用域的引入,主要是为了解决使用 var 声明变量时可能产生的变量提升作用域混乱的问题。

具体来说,块级作用域有以下几个特点:

  • 限定变量可见性: 变量仅在其声明的代码块内部有效,超出这个代码块的范围就无法访问。

  • 避免变量污染: 通过块级作用域,可以避免在不同代码块中意外地使用相同的变量名,从而减少错误。

  • 支持更精确的控制: 块级作用域让开发者可以更精确地控制变量的生命周期和作用范围,提高代码的可维护性和可读性。

let、const、var的区别

(1)块级作用域:块作用域由 { } 包括,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:

  • 内层变量可能覆盖外层变量
  • 用来计数的循环变量泄露为全局变量

(2)变量提升:var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错。

(3)给全局添加属性:浏览器的全局对象是 window,Node的全局对象是 global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。

(4)重复声明:var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。

(5)暂时性死区:在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。

(6)初始值设置:在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。

(7)指针指向:let 和 const 都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。

const对象的属性可以修改吗

const 保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。对于基本类型的数据(数值、字符串、布尔值),其值就保存在变量指向的那个内存地址,因此等同于常量。

但对于引用类型的数据(主要是对象和数组)来说,变量指向数据的内存地址,保存的只是一个指针,const只能保证这个指针是固定不变的,至于它指向的数据结构是不是可变的,就完全不能控制了。

箭头函数与普通函数的区别

(1)this 绑定

  • 普通函数有自己的 this 值,取决于它是如何被调用的。
  • 箭头函数没有自己的 this 值,它会捕获其所在上下文的 this 值。所以箭头函数中this的指向在它在定义时已经确定了,之后不会改变。
javascript
const id = 'GLOBAL';
const obj = {
  id: 'OBJ',
  a: function() {
    console.log(this.id);
  },
  b: () => {
    console.log(this.id);
  }
};
obj.a();    // 'OBJ'
obj.b();    // 'GLOBAL'
new obj.a()  // undefined
new obj.b()  // Uncaught TypeError: obj.b is not a constructor

对象 obj 的方法 b 是使用箭头函数定义的,这个函数中的 this 就永远指向它定义时所处的全局执行环境中的 this,即便这个函数是作为对象 obj 的方法调用,this依旧指向 Window 对象。需要注意,定义对象的大括号 {} 是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中。

call()apply()bind()等方法不能改变箭头函数中 this 的指向

javascript
const id = 'Global';
const fun1 = () => {
  console.log(this.id)
};
fun1();                       // 'Global'
fun1.call({ id: 'Obj' });     // 'Global'
fun1.apply({ id: 'Obj' });    // 'Global'
fun1.bind({ id: 'Obj' })();   // 'Global'

(2)arguments 对象

  • 普通函数有 arguments 对象,包含所有传入的参数。
  • 箭头函数没有 arguments 对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。

(3)箭头函数没有 prototype

(4)箭头函数不能用作 Generator 函数,不能使用 yeild 关键字

箭头函数的this指向哪⾥?

箭头函数不同于传统JavaScript中的函数,箭头函数并没有属于⾃⼰的this,它所谓的this是捕获其所在上下⽂的 this 值,作为⾃⼰的 this 值,并且由于没有属于⾃⼰的this,所以是不会被new调⽤的,这个所谓的this也不会被改变。

可以⽤Babel理解⼀下箭头函数:

javascript
// ES6 
const obj = { 
  getArrow() { 
    return () => { 
      console.log(this === obj); 
    }; 
  } 
}

转化后

javascript
// ES5,由 Babel 转译
var obj = { 
   getArrow: function getArrow() { 
     var _this = this; 
     return function () { 
        console.log(_this === obj); 
     }; 
   } 
};

new 操作符都做了什么

  1. 创建一个对象
  2. 将构造函数的作用域赋给新对象(也就是将对象的__proto__属性指向构造函数的prototype属性)
  3. 指向构造函数中的代码,构造函数中的this指向该对象(也就是为这个对象添加属性和方法)
  4. 返回新的对象

手写 new 操作符

如果 new 一个箭头函数的会怎么样

箭头函数是ES6中的提出来的,它没有prototype,也没有自己的 this指向,更不可以使用 arguments参数,所以不能 new一个箭头函数。

new 操作符的实现步骤如下:

  1. 创建一个对象
  2. 将构造函数的作用域赋给新对象(也就是将对象的__proto__属性指向构造函数的prototype属性)
  3. 指向构造函数中的代码,构造函数中的this指向该对象(也就是为这个对象添加属性和方法)
  4. 返回新的对象

所以,上面的第二、三步,箭头函数都是没有办法执行的

for...in和for...of的区别

for...infor...of 都是JavaScript 的循环语句,但它们的应用场景和遍历方式有所不同。

for...in 主要用于遍历对象的可枚举属性,获取的是 键名,而 for...of 用于遍历可迭代对象(如数组、字符串、Map、Set等)的值,获取的是键值。一个数据结构只要部署了 Symbol.iterator 属性, 就被视为具有 iterator 接口, 就可以使用 for...of 循环。

特点for...infor...of
适用对象对象(普通对象和原型链上的可枚举属性)可迭代对象(数组、字符串、Map、Set等)
迭代内容属性名(键名)元素值
遍历顺序不确定迭代器顺序
原型链遍历原型链上的可枚举属性不遍历原型链

call、apply、bind

callapplybind 都是 JavaScript 中用于改变函数 this 指向的方法,它们都接收一个对象作为第一个参数,用于指定函数执行时的 this 值。

callapply 立即执行函数,而 bind 返回一个绑定了指定 this 值的新函数,需要手动调用才能执行。

  1. call()
  • call() 接收一个对象作为第一个参数,该对象将成为函数执行时的 this 值。如果指定了 null 或者 undefined 则内部 this 指向 window。
  • 后面的参数会被依次传递给函数作为参数。
  • call() 会立即执行函数

手写call函数

  1. apply()
  • apply() 与 call() 类似,也接收一个对象作为第一个参数,指定 this 值。
  • 不同之处在于,apply() 接收一个数组作为第二个参数,该数组的元素会作为参数传递给函数。
  • apply() 也会立即执行函数。

手写applay函数

  1. bind()
  • bind() 同样接收一个对象作为第一个参数,用于指定函数执行时的 this 值。
  • 后面的参数会被依次传递给函数作为参数,就像 call() 一样。
  • bind() 不会立即执行函数,而是返回一个绑定了指定 this 值的新函数。这个新函数可以稍后被调用,并且在调用时,this 永远指向 bind 时指定的对象。

手写bind函数

use strict是什么意思 ? 使用它区别是什么?

use strict 是一种 ECMAscript5 添加的(严格模式)运行模式,这种模式使得 Javascript 在更严格的条件下运行。设立严格模式的目的如下:

  • 消除 Javascript 语法的不合理、不严谨之处,减少怪异行为;
  • 消除代码运行的不安全之处,保证代码运行的安全;
  • 提高编译器效率,增加运行速度;
  • 为未来新版本的 Javascript 做好铺垫。

区别

  • 不允许使用未声明的变量。
  • 不允许删除变量、函数和对象。
  • 不允许变量重名。
  • 不允许使用八进制。
  • 不允许使用转义字符。
  • 不允许对只读属性赋值。
  • 不允许对一个使用getter方法读取的属性进行赋值。
  • 不允许删除一个不可删除的属性。
  • 不允许使用 eval() 创建变量。
  • 不允许使用 arguments.callee 和 arguments.caller。
  • 不允许使用 fn.caller 和 fn.arguments 获取函数调用的堆栈。
  • 参数的值不会随 arguments 对象的值的改变而变化。
  • 禁止在函数内部遍历调用栈。

提取嵌套多层对象的属性

有时会遇到一些嵌套程度非常深的对象:

javascript
const school = {
   classes: {
      stu: {
         name: 'Bob',
         age: 24,
      }
   }
}

有一种标准的做法

const { classes: { stu: { name } }} = school
       
console.log(name)  // 'Bob'

可以在解构出来的变量名右侧,通过冒号+{目标属性名}这种形式,进一步解构它,一直解构到拿到目标数据为止。

对 rest 参数的理解

rest 参数(形式为 ...变量名)是 ES6 引入的新特性,允许我们将一个不定数量的参数表示为一个数组。

  • 收集多余参数: rest 参数用于收集函数调用时传入的,但没有对应形参的参数。它会将这些多余的参数组合成一个数组。

  • 位置限制: rest 参数必须是函数参数列表中的最后一个参数。

  • 数组类型: rest 参数是一个真正的 Array 实例,可以直接使用 sortmapforEach 等数组方法,而 arguments 对象则不是一个真正的数组。

  • 替代 arguments: ES6 之后,rest 参数可以替代 arguments 对象,用于获取函数的多余参数,并且更加简洁易用。

javascript
function sum(...args) {
  return args.reduce((prev, curr) => prev + curr, 0);
}

sum(1, 2, 3, 4, 5); // 15

arguments 对象与rest 参数的区别

  • arguments 对象包含了函数调用时传入的所有参数,而 rest 参数只包含没有对应形参的多余参数。
  • arguments 对象不是一个数组,而 rest 参数是一个真正的数组。
  • arguments 对象有 callee 等其他属性,而 rest 参数没有。

js脚本延迟加载

延迟加载就是等页面加载完成之后再加载脚本文件。 js 延迟加载有助于提高页面加载速度。

一般有以下几种方式:

defer 属性:给 script 标签添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。

async 属性:给 script 标签添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。

动态创建 DOM 方式:动态创建 DOM 标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。

使用 setTimeout 延迟方法:设置一个定时器来延迟加载js脚本文件

让 JS 最后加载:将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。

escape、encodeURI、encodeURIComponent 的区别

encodeURI 是对完整的 URI 进行转义,将 URI 中的非法字符转换为合法字符,所以对于一些在 URI 中有特殊意义的字符 !@$&'()*+,/:;=?@-._~ 等不会进行转义。

encodeURIComponent 是对 URI 的组成部分进行转义,会假定它的参数是 URI 的一部分,所以一些特殊字符也会得到转义。

escape 和 encodeURI 的作用相同,不过它们对于 unicode 编码为 0xff 之外字符的时候会有区别,escape 是直接在字符的 unicode 编码前加上 %u,而 encodeURI 首先会将字符转换为 UTF-8 的格式,再在每个字节前加上 %。

什么是尾调用,使用尾调用有什么好处?

尾调用指的是函数的最后一步调用另一个函数。

代码执行是基于执行栈的,所以当在一个函数里调用另一个函数时,会保留当前的执行上下文,然后再新建另外一个执行上下文加入栈中。

使用尾调用的话,因为已经是函数的最后一步,所以这时可以不必再保留当前的执行上下文,从而节省了内存,这就是尾调用优化。

但是 ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

变量提升和函数提升

  • 变量提升(Variable Hoisting) 指的是在 JS 代码执行前,会将使用 var 声明的变量提升到其作用域的顶部,但变量的赋值操作不会提升。这意味着,即使你在声明变量之前使用它,也不会报错,但变量的值会是 undefined。
javascript
console.log(a); // undefined
var a = 1;
console.log(a); // 1
  • 函数提升(Function Hoisting) 指的是在 JS 代码执行前,会将使用 function 声明的函数提升到其作用域的顶部,但函数的赋值操作不会提升。需要注意的是,使用函数表达式(例如 var myFunction = function() {})声明的函数,其提升效果和变量提升一样,只有声明会被提升,赋值操作不会提升。
javascript
console.log(foo); // ƒ foo() { console.log('foo'); }
function foo() {
  console.log('foo');
}
javascript
console.log(foo); // undefined
var foo = function() {
  console.log('foo');
}

造成变量声明提升的本质原因是 js 引擎在代码执行前有一个解析的过程,创建了执行上下文,初始化了一些代码执行时需要用到的对象。当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。

那为什么会进行变量提升呢?主要有以下两个原因:

(1)提高性能

在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),而这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。

在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、不必要的空白等。这样做的好处就是每次执行函数时都可以直接为该函数分配栈空间(不需要再解析一遍去获取代码中声明了哪些变量,创建了哪些函数),并且因为代码压缩的原因,代码执行也更快了。

(2)容错性更好

变量提升可以在一定程度上提高JS的容错性,看下面的代码:

javascript
a = 1;
var a;
console.log(a); // 1

如果没有变量提升,这两行代码就会报错,但是因为有了变量提升,这段代码就可以正常执行。

虽然,在可以开发过程中,可以完全避免这样写,但是有时代码很复杂的时候。可能因为疏忽而先使用后定义了,这样也不会影响正常使用。由于变量提升的存在,而会正常运行。

总结

  • 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间
  • 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行

变量提升虽然有一些优点,但是他也会造成一定的问题,在ES6中提出了 letconst 来定义变量,它们就没有变量提升的机制。下面看一下变量提升可能会导致的问题:

  1. 变量提升导致内层变量覆盖外层变量
javascript
var scope = 'global';

function fn() {
	console.log(scope);
	if (false) {
		var scope = 'local';
	}
}

fn(); // undefined

在这个函数中,原本是要打印出外层的 scope 变量,但是因为变量提升的问题,内层定义的 scope 被提到函数内部的最顶部,相当于覆盖了外层的 scope,所以打印结果为 undefined,等效于下面代码:

javascript
var scope = 'global';

function fn() {
	var scope;
  console.log(scope);
  if (false) {
    scope = 'local';
  }
}

fn(); // undefined
  1. 本应销毁的变量没有被销毁
javascript
function foo(){
  for (var i = 0; i < 7; i++) {
  }
  console.log(i); 
}
foo(); // 7

在创建执行上下文阶段,变量 i 就已经被提升了,所以当for循环结束之后,变量 i 并没有被销毁。

什么是构造函数

构造函数本身就是一个函数,它与普通函数的区别在于调用方式不同。普通函数是直接调用,而构造函数是通过 new 关键字调用。

javascript
function Person(name, age) {
  this.name = name;
  this.age = age;
}

const john = new Person('John', 20);

john.constructor === Person; // true
john.constructor === Object; // false

constructor 是一个特殊的属性,存在于每个对象(包括对象实例和原型)的内部,并且指向创建该对象的函数。简单来说,constructor 属性告诉我们这个对象是用哪个函数创建的。

  • 除了 null 原型对象之外,任何对象都会在其原型对象上有一个 constructor 属性。使用字面量创建的对象也会有一个指向该对象构造函数类型的 constructor 属性。

  • Symbol() 构造函数返回一个 symbol 类型的值,但是它并不完全支持构造函数的语法,因为它不支持 new Symbol() 语法,也无法被子类化。

  • constructor 属性没有受到保护,可以被重新赋值或被覆盖。因此在检测变量类型时,通常应避免使用它,而应该使用更不易出错的方法,如对于对象使用 instanceofSymbol.toStringTag,对于基本类型使用 typeof

对原型、原型链的理解

JavaScript是一种基于原型的语言,每个对象拥有一个原型对象,对象以其原型为模板,从原型继承方法和属性,这些属性和方法定义在对象的构造函数prototype 属性上,而非对象实例本身。

在实际编程中,通过 __proto__(一个非标准但被广泛支持的属性) 和 Object.getPrototypeOf()(标准方法) 可以直接访问对象的原型对象。

javascript
function Person(name, age) {
  this.name = name;
  this.age = age;
};

Person.prototype.sayHello = function() {
  console.log('Hello, I am ' + this.name);
};

var john = new Person('John', 20);

john.sayHello(); // Hello, I am John

john.__proto__ === Person.prototype; // true

Object.getPrototypeOf(john) === Person.prototype; // true

当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,而原型对象也是一个对象,它也有自己的原型对象,于是就这样一直找下去,直到找到该属性,或者到达链的末端。这种关系被称为原型链 (prototype chain)。

原型链

原型链的末端是 null,它没有原型对象。

MDN 文档

实现继承的方式有哪些

  1. 原型链继承

  2. 构造函数继承

  3. 组合继承

  4. 原型式继承

  5. 寄生式继承

  6. 寄生组合式继承

  7. 混入继承

对闭包的理解

作用域和作用域链

对执行上下文的理解

事件循环机制

宏任务和微任务分别有哪些

异步编程的实现方式

对 Ajax 的理解

对 Promise 的理解

对 async/await 的理解

await 到底在等啥

内存泄漏

模块化