前言
随着前端知识体系的不断壮大,面试时候问的问题越来越杂,越来越多。经常会看到一些大神在面经上说前端的知识理解就好,要形成自己的知识体系,不用去背。不过很长一段时间,我都是在背,感觉知识点一直都在缝缝补补,所以特意写了这篇文章,根据常见的数据类型相关面试题梳理一下自己的知识体系。
一、let、const、var区别
1、作用域
let和const具有块级作用域,var声明的变量具有函数作用域和全局作用域
-
全局作用域:在脚本中任何地方都可以访问的作用域,不过过多的全局作用域变量会污染全局命名空间,引起命名冲突。
(1)、所有未定义直接赋值的变量自动声明为全局作用域
function example() { a = "桀桀桀"; // 未使用 var/let/const 声明 } example(); console.log(a); // 桀桀桀
(2)、最外层函数和最外层函数之外定义的变量拥有全局作用域
var a = "桀桀桀"; function example() { console.log("1", a); // 桀桀桀 } example(); console.log("2", a); // 桀桀桀
(3)、所有window(浏览器环境)、global(Node.js环境)对象的属性拥有全局作用域
var a = "桀桀桀"; console.log("1", window.a); // 桀桀桀 function example() { console.log("2", a); // 桀桀桀 } example();
-
函数作用域:在函数内部声明的变量只能在该函数内部访问,而不能在函数外部访问。
(1)、函数内部声明的变量只能在该函数内访问
function example() { var a = "桀桀桀"; console.log("1", a); // 桀桀桀 } example(); console.log("2", a); // a is not defined
-
块级作用域:由一对大括号
{}
包裹的代码片段中声明的变量,作用范围只限于这对大括号内。块级作用域解决了ES5中的两个问题,内层变量可能覆盖外层变量,用来计数的循环变量泄漏为全局变量。(1)、使用 let 和 const 声明块级作用域:在 ES6 之前,JavaScript 没有块级作用域,
var
声明的变量仅有函数作用域。而let
和const
引入后,允许在块级代码(如if
、for
、while
等控制结构)中创建局部变量。if (true) { let blockVar = "I am inside a block"; console.log(blockVar); // 输出 "I am inside a block" } console.log(blockVar); // 报错:blockVar is not defined
(2)、循环结构中,
let
和const
非常适合用于计数器变量,因为它们能够将变量限制在循环体内部,避免在循环外部对计数器的意外访问。for (let i = 0; i < 5; i++) { setTimeout(() => console.log(i), 1000); // 输出 0, 1, 2, 3, 4 } for (var j = 0; j < 5; j++) { setTimeout(() => console.log(j), 1000); // 输出 5, 5, 5, 5, 5 }
作用域链:是一层层的关系链,用来在嵌套的函数或代码块中查找变量。它将当前作用域与外部作用域联系起来,直到找到全局作用域(如 window
对象)。作用域链的本质是一个指向变量对象的指针列表。每当 JavaScript 创建一个执行上下文时,它也会创建一个作用域链,用来存储执行上下文中所有变量和函数的引用。
let globalVar = "global";
function outerFunction() {
let outerVar = "outer";
function innerFunction() {
let innerVar = "inner";
console.log(innerVar); // 输出 "inner"
console.log(outerVar); // 输出 "outer",从外层作用域查找
console.log(globalVar); // 输出 "global",从全局作用域查找
}
innerFunction();
}
outerFunction();
执行上下文:定义了代码的执行环境,决定了变量、对象和函数的访问权限和行为。每当 JavaScript 代码运行时,都会创建相应的执行上下文。
-
分类:
(1)、全局执行上下文:任何不在函数内部的代码都在全局执行上下文中执行,全局执行上下文创建了一个全局对象(在浏览器环境中是
window
,在 Node.js 环境中是global
),并且将this
绑定到这个全局对象。一个 JavaScript 程序中只能有一个全局执行上下文。(2)、函数执行上下文:每当一个函数被调用时,JavaScript 引擎为该函数创建一个新的执行上下文。- 每个函数都有自己独立的执行上下文,函数执行上下文包含函数内部的变量、参数和
this
的引用。一个程序中可以有多个函数执行上下文,它们是动态创建和销毁的。(3)、Eval 执行上下文:当
eval
函数被调用时,会为其创建一个独立的执行上下文。这种上下文通常很少使用。// `eval` 函数的语法非常简单,接收一个字符串,并将该字符串作为 JavaScript 代码执行。`eval` 可以执行包括表达式、变量声明、函数声明等。 eval("JavaScript 代码的字符串");
-
组成:
(1)、变量对象:变量对象是一个包含了上下文中所有变量、函数声明和参数的对象。在创建阶段,函数声明会首先存储在变量对象中,而变量只存储声明(存储名称,但未初始化,初始化在执行阶段完成)。
(2)、作用域链:每个执行上下文都有自己的作用域链,它用于保证代码在执行时能够顺利访问到所需的变量。
(3)、
this
绑定:this关键字的值是在执行上下文创建时确定的,它指向当前执行上下文所绑定的对象(变量对象),在全局执行上下文中,this指向全局对象(浏览器中是window
,Node.js 中是global
)。在函数执行上下文中,this的值取决于函数的调用方式,可能指向调用者对象、全局对象,或者在严格模式下undefined
。 -
生命周期:每个执行上下文都有三个主要的阶段,它们决定了上下文是如何创建、初始化以及如何执行的
(1)、创建阶段:
- this绑定
- 创建词法环境组件:用于保存标识符和它们的变量绑定关系。词法环境本质上是一个对象,包含两个部分,
环境记录器
(用来存储变量和函数声明的实际位置,函数声明会被完整提升,而变量声明只会提升其名称,赋值则会在执行阶段进行。)、外部环境引用
(指向父级作用域,形成作用域链,用于从父级作用域访问变量。) - 创建变量环境组件:变量环境本质上也是一个词法环境,但它专门用于处理
var
声明的变量。环境记录器
变量环境的环境记录器存储通过var
声明的变量,它只适用于函数作用域或全局作用域,块级作用域不会影响它。
(2)、执行阶段:JavaScript 引擎开始执行代码,将变量赋值,执行函数等。
-
执行上下文栈:
(1)JavaScript引擎使用执行上下文栈来管理执行上下文
(2)当JavaScript执行代码时,首先遇到全局代码,会创建一个
全局执行上下文
并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数
,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文。
2、变量提升
var
:有变量提升,意味着变量的声明会被提升到当前作用域的顶部。在提升之前变量的值是 undefined
。
console.log(a); // 输出 undefined
var a = 10;
let
和 const
:没有变量提升,在声明之前使用变量会引发错误,这就是 “暂时性死区”。
console.log(b); // 报错:Cannot access 'b' before initialization
let b = 10;
3、给全局对象添加属性
var
:在全局作用域中使用 var
声明的变量会成为全局对象(浏览器中的 window
或 Node.js 中的 global
)的属性。
let
和 const
:不会将变量绑定到全局对象。即使它们在全局作用域中声明,window
或 global
对象上也没有这些变量。
4、重复声明
var
:可以重复声明同一个变量,后声明的会覆盖之前的声明。
var x = 1;
var x = 2;
console.log(x); // 输出 2
let
和 const
:不能重复声明同一个变量,如果尝试重复声明,会引发错误。
5、初始值设置
var
和 let
:允许声明变量时不赋初始值,变量的值会是 undefined
,直到手动赋值为止。
let a;
console.log(a); // 输出 undefined
a = 5;
const
:必须在声明时赋初始值,否则会报错。
const b; // 报错:Missing initializer in const declaration
6、指针指向
let
:允许变量的指针指向改变,即可以重新赋值。
let x = 10;
x = 20; // 允许重新赋值
const
:不能改变指针指向,声明的变量值不能被重新赋值。但是对于对象或数组,const
只是锁定了引用的地址,对象的内容或数组的元素可以修改。
const y = 30;
y = 40; // 报错:Assignment to constant variable
const arr = [1, 2, 3];
arr.push(4); // 合法,修改了数组内容
console.log(arr); // 输出 [1, 2, 3, 4]
const obj = { name: 'Alice' };
obj.age = 25; // 合法,修改了对象内容
console.log(obj); // 输出 { name: 'Alice', age: 25 }
二、JavaScript的数据类型
1、数据类型分类
JavaScript共有八种数据类型,可分为两大类,分别是原始数据类型(7种)和引用数据类型(1种)
(1)原始数据类型
- Undefined:当声明了一个变量但未给它赋值时,该变量的值就是
undefined
。 - Null:
null
是一个表示“空”的特殊值,通常用于表示一个空对象引用。 - Boolean:
Boolean
类型只有两个值:true
和false
,用于逻辑操作。 - Number:
Number
类型用于表示整数和浮点数。JavaScript 中所有的数字都是 64 位浮点数。由于Number
类型的限制,它只能安全地表示-(2^53-1)
到2^53-1
之间的整数(即Number.MAX_SAFE_INTEGER
和Number.MIN_SAFE_INTEGER
)。 - String:
String
类型表示字符串 - Symbol(ES6 引入):
Symbol
是一种新的原始数据类型,用于创建独一无二的标识符。每个通过Symbol()
生成的符号都是唯一的,不能与其他符号相等。 - BigInt(ES6 引入):
BigInt
是一种用于表示任意精度整数的数据类型,可以存储大于Number
类型最大安全整数范围的整数。
(2)引用数据类型
- Object: 是 JavaScript 中最复杂的数据类型,用于表示
对象、数组、函数
等。对象是属性的集合,属性由键值对组成。
2、原始数据类型与引用数据类型的区别
(1)存储位置
原始数据类型
:存储在栈内存中。栈内存是大小固定的,因此存储简单数据结构非常快速,适合用于存储固定大小的原始数据类型。
引用数据类型
:存储在堆内存中。由于对象的大小不固定,堆内存用来存储较大或动态的数据。栈中存储的是对堆中对象的指针(引用地址),而对象本身保存在堆中。
(2)堆和栈的区别
栈
- 数据结构中的栈是先进后出的结构,存取操作非常快,适合用于小数据的快速存储和管理。
- 在操作系统中,栈内存是由编译器自动分配和管理的,适合存储简单的数据(如局部变量、函数参数等)。
堆
- 数据结构中的堆通常是一个优先队列,数据按照一定的优先级进行排列。
- 在操作系统中,堆内存用于存储动态分配的内存。堆内存的分配和释放通常由开发者或垃圾回收器管理。
3、数据类型检测方式
(1)typeof
用于返回一个值的数据类型。适用于检测基本类型,无法区分对象类型,如数组和对象
console.log(typeof 42); // "number"
console.log(typeof 'hello'); // "string"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object" (这是一个历史遗留问题)
console.log(typeof {}); // "object"
console.log(typeof []); // "object"
console.log(typeof function() {}); // "function"
注意1
:typeof null 的结果是Object,这是一个历史遗留的问题。
在 JavaScript 第一个版本中,所有值都存储在 32 位的单元中,每个单元包含一个小的 类型标签 以及当前要存储值的真实数据。类型标签存储在每个单元的低位中,共有五种数据类型。
000: object - 当前存储的数据指向一个对象。
1: int - 当前存储的数据是一个 31 位的有符号整数。
010: double - 当前存储的数据指向一个双精度的浮点数。
100: string - 当前存储的数据指向一个字符串。
110: boolean - 当前存储的数据是布尔值。
由于null指针的值全是0,所以null的类型标签也是000,和Object的类型标签一样,所以会被判定为Object。
-
注意2
:typeof NaN的结果是Number,NaN 指“不是一个数字”。NaN 是一个特殊值,它和自身不相等,NaN !== NaN 为 true。判断NaN需要isNaN 和 Number.isNaN
函数。isNaN
:接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。Number.isNaN
:会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。
(2)instanceof
instanceof
用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。
instanceof
只能正确判断引用数据类型,而不能判断基本数据类型。
console.log(1 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
function myInstanceOf(left, right){
// Object.getPrototypeOf获取原型是一个只读操作,不会像left.__proto__可能会存在操作内部属性
let proto = Object.getPrototypeOf(left)
let prototype = right.prototype
while(true){
if(!proto) return false;
if(proto === prototype) return true;
proto = Object.getPrototypeOf(proto)
}
}
(3)constructor
constructor
是对象的原型属性之一,指向创建该对象实例的构造函数。有两个作用
-
判断数据的类型:通过constructor属性可以获取对象的构造函数,从而判断对象的类型。
-
对象实例访问其构造函数:通过对象的constructor属性,可以访问用于创建该对象的构造函数。
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
就不能用来判断数据类型了
// 根据原型链可知,f.constructor和f.__proto__.constructor是一样的(原型链的继承),f.__proto__就是Fn.
// prototype,Fn.prototype.constructor本来应该指向构造函数Fn,但是现在Fn.prototype是Array的实例,根据实例的
// constructor属性指向其构造函数,可知Fn.prototype.constructor指向Array,所以f.constructor也是指向Array
function Fn(){};
Fn.prototype = new Array();
var f = new Fn();
console.log(f.constructor===Fn); // false
console.log(f.constructor===Array); // true
(4)Object.prototype.toString.call()
Object.prototype.toString.call()
使用 Object 对象的原型方法 toString 来判断数据类型,但是obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样,因为toString是Object的原型方法,而Array、function等类型作为Object的实例,都重写了toString方法。不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应的重写之后的toString方法(function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…),而不会去调用Object上原型toString方法(返回对象的具体类型)。
console.log(Object.prototype.toString.call(123)); // "[object Number]"
console.log(Object.prototype.toString.call("hello")); // "[object String]"
console.log(Object.prototype.toString.call(true)); // "[object Boolean]"
console.log(Object.prototype.toString.call(null)); // "[object Null]"
console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]"
console.log(Object.prototype.toString.call([])); // "[object Array]"
console.log(Object.prototype.toString.call({})); // "[object Object]"
console.log(Object.prototype.toString.call(new Date())); // "[object Date]"
console.log(Object.prototype.toString.call(/regex/)); // "[object RegExp]"
console.log(Object.prototype.toString.call(new Error())); // "[object Error]"
console.log(Object.prototype.toString.call(function() {})); // "[object Function]"
4、undefined类型
(1)获取安全的 undefined 值
因为undefined是一个标识符,所以可以被当作变量来使用和赋值,但是这样会影响 undefined 的正常判断。表达式 void ___
没有返回值,因此返回结果是undefined。也可以用 void 0
来获得 undefined。
(2)注意
当变量声明还是未声明,typeof返回的都是字符串 undefined ,虽然两个变量存在根本性差异,但它们都无法执行实际操作
let a;
console.log(typeof a); // "undefined"
console.log(typeof b); // "undefined"
5、为什么0.1+0.2 ! == 0.3
计算机是通过二进制的方式存储数据的,所以计算机计算0.1+0.2的时候,实际上是计算的两个数的二进制的和。一般我们认为数字包括整数和小数,但是在 JavaScript 中只有一种数字类型:Number。
Number类型是采用IEEE754标准64位存储的双精度浮点数。为每个数值分配64位存储空间,以科学计数法的方式存储。64位存储空间包括1位符号位(0正1负),11位指数位(1023 + 指数实际值),52位小数位(2进制小数点后的数字)。
计算二进制
:整数部分是不断除2取余直到商为零 + 逆序排列,小数部分是不断乘2取整直到小数部分为零 + 顺序排列。
// 0.1的二进制,也是0011不断循环,不过头部多一位0,可得第53位是1
0.0001100110011001100110011001100110011001100110011...
// 0.2的二进制,0011不断循环,可得53位是0
0.001100110011001100110011001100110011001100110011...
52位后需要舍去,遵从“0舍1入”的原则。所以0.1的二进制,需要向前进一位,这里就发生了第一次精度丢失。之后0.1 + 0.2第二次精度丢失,所以0.1+0.2 ! == 0.3。
解决办法
-
使用toFixed()后再使用parseFloat
parseFloat((0.1 + 0.2) .toFixed(10))
-
使用整数运算
const result = (0.1 * 10 + 0.2 * 10) / 10; console.log(result === 0.3); // 输出: true
-
使用
Number.EPSILON
在ES6中,提供了
Number.EPSILON
属性,而它的值就是2-52,只要判断0.1+0.2-0.3
是否小于Number.EPSILON
,如果小于,就可以判断为0.1+0.2 ===0.3function isEqual(a, b) { return Math.abs(a - b) < Number.EPSILON; } console.log(isEqual(0.1 + 0.2, 0.3)); // 输出: true
-
使用
Decimal.js
const Decimal = require('decimal.js'); const result = new Decimal(0.1).plus(0.2); console.log(result.equals(0.3)); // 输出: true
6、BigInt的提案
因为 JavaScript 使用的是 IEEE 754 双精度浮点数格式,这种格式只能精确表示 Number.MAX_SAFE_INTEGER(-(2^53 - 1)
) 到 Number.MAX_SAFE_INTEGER(2^53 - 1
)之间的整数。当超出安全整数范围的运算会导致精度丢失,结果不再可靠,这在⼤数计算的时候不得不依靠⼀些第三⽅库进⾏解决,因此官⽅提出了BigInt
来解决此问题。
BigInt一些特点
-
BigInt
支持所有常见的算术运算(加、减、乘、除等),但不支持浮点运算const a = 10n; const b = 20n; console.log(a + b); // 输出: 30n
-
BigInt
不支持小数,只能表示整数。如果对BigInt
进行除法运算,结果将被截断为整数const result = 10n / 3n; console.log(result); // 输出: 3n (舍去小数部分)
-
BigInt
和Number
不能直接进行运算,必须将其中之一转换为相同类型const num = 10; const bigInt = 20n; console.log(bigInt + BigInt(num)); // 转换 `Number` 为 `BigInt` console.log(Number(bigInt) + num); // 转换 `BigInt` 为 `Number`
7、判断一个对象是空对象
- (1)使用
Object.keys()
、Object.values()
、Object.entries()
Object.keys()
方法返回一个对象自身可枚举属性名的数组,通过判断这个数组的长度是否为 0,就可以确定对象是否为空。
Object.values()
方法返回一个对象自身可枚举属性值的数组,通过判断这个数组的长度是否为 0。
Object.entries()
返回对象自身的可枚举属性的键值对数组。通过检查数组的长度来判断对象是否为空。
const person = {
name: 'Alice',
age: 25,
occupation: 'Engineer'
};
console.log(Object.keys(person));
// 输出: ['name', 'age', 'occupation']
console.log(Object.values(person));
// 输出: ['Alice', 25, 'Engineer']
console.log(Object.entries(person));
// 输出: [['name', 'Alice'], ['age', 25], ['occupation', 'Engineer']]
- (2)使用
for...in
循环
for...in
循环用于遍历对象的所有可枚举属性,如果对象没有任何属性,循环体将不会执行
const obj = {};
let isEmpty = true;
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
isEmpty = false;
break;
}
}
console.log(isEmpty ? "对象是空的" : "对象不是空的");
- (3)使用
JSON.stringify()
将对象转换为 JSON 字符串来判断对象是否为空。如果对象为空,则转换后的字符串会是 "{}"
const obj = {};
if (JSON.stringify(obj) === "{}") {
console.log("对象是空的");
} else {
console.log("对象不是空的");
}
- (4)使用
Object.getOwnPropertyNames()
Object.getOwnPropertyNames()
返回对象的所有属性名(包括不可枚举属性),通过判断返回数组的长度来判断对象是否为空。
const obj = {};
if (Object.getOwnPropertyNames(obj).length === 0) {
console.log("对象是空的");
} else {
console.log("对象不是空的");
}
8、for...in和for...of的区别
-
for...in
:适用于对象,但也可以用于数组等数据结构,不过不推荐,因为它遍历的是属性名(索引),而不是数组的元素。for...of
是 ES6 中新增的遍历方式,它可以遍历所有可迭代对象,例如数组、字符串、Map
、Set
等,但普通的对象(即不具备Symbol.iterator
接口的对象)是不能直接使用for...of
遍历的,试图用for...of
遍历普通对象时会报错。const person = { name: 'Alice', age: 25, occupation: 'Engineer' }; for (let key in person) { console.log(key); // 输出: 'name', 'age', 'occupation' } const obj = { a: 1, b: 2 }; for (let value of obj) { console.log(value); // TypeError: obj is not iterable }
-
for...in
:遍历的是对象的可枚举属性名(包括对象自身的属性以及从原型链继承的可枚举属性)for...of
:遍历的是可迭代对象(如数组、字符串、Map
、Set
等)的值 -
for...in
遍历的是数组的可枚举属性,而稀疏数组中的“空位”(即没有赋值的索引)不会被枚举到。因此,for...in
会跳过这些空位,不会遍历到它们。for...of
遍历的是数组的值,即使某些位置是空的或没有定义的索引,它仍然会返回undefined
作为空位的值。因此,for...of
不会跳过稀疏数组中的空位,而是会遍历到每个位置,即使值为undefined
。const sparseArray = [1, , 3]; // 稀疏数组,中间有一个空位 for (let index in sparseArray) { console.log(index, sparseArray[index]); } // 输出: // 0 1 // 2 3 const sparseArray = [1, , 3]; // 稀疏数组,中间有一个空位 for (let value of sparseArray) { console.log(value); } // 输出: // 1 // undefined // 3
9、类数组
类数组对象是指具有数组部分特征的对象,有以下几个特点
-
有
length
属性:表示对象包含的元素数量。 -
使用索引访问元素:可以通过类似数组的方式访问元素,如
obj[0]
,obj[1]
。 -
不具有数组的方法:例如
push
、forEach
、map
等数组方法const arrayLike = { 0: 'apple', 1: 'banana', 2: 'cherry', length: 3 }; console.log(arrayLike[0]); // 输出: 'apple' console.log(arrayLike.length); // 输出: 3
如何将类数组对象转换为数组
-
(1)
Array.from()
const arrayLike = { 0: 'apple', 1: 'banana', 2: 'cherry', length: 3 }; const arr = Array.from(arrayLike); console.log(arr); // 输出: ['apple', 'banana', 'cherry'] console.log(Array.isArray(arr)); // 输出: true
-
(2)
Array.prototype.slice.call()
const arrayLike = { 0: 'apple', 1: 'banana', 2: 'cherry', length: 3 }; const arr = Array.prototype.slice.call(arrayLike); console.log(arr); // 输出: ['apple', 'banana', 'cherry']
-
(3)
Array.prototype.splice.call(arrayLike, 0)
const arrayLike = { 0: 'apple', 1: 'banana', 2: 'cherry', length: 3 }; const arr = Array.prototype.splice.call(arrayLike, 0); console.log(arr); // 输出: ['apple', 'banana', 'cherry']
-
(4)
Array.prototype.concat.apply([], arrayLike);
const arrayLike = { 0: 'apple', 1: 'banana', 2: 'cherry', length: 3 }; const arr = Array.prototype.concat.apply([], arrayLike); console.log(arr); // 输出: ['apple', 'banana', 'cherry']
10、数组的遍历方法
-
(1)
forEach()
:不改变原数组,没有返回值,对每个数组元素执行操作,不能中途跳出循环。let arr = [1, 2, 3, 4]; arr.forEach(function(value, index, array) { console.log(value); });
-
(2)
map()
:不改变原数组,有返回值,返回一个新数组,每个元素是对原数组元素执行操作后的结果。let arr = [1, 2, 3, 4]; let newArr = arr.map(value => value * 2); console.log(newArr); // [2, 4, 6, 8]
-
(3)
filter()
:返回一个新数组,包含所有通过测试(回调函数返回 true)的元素let arr = [1, 2, 3, 4]; let filteredArr = arr.filter(value => value > 2); console.log(filteredArr); // [3, 4]
-
(4)
reduce() 和 reduceRight()
:对数组的每个元素执行回调函数,将其累积为一个单一值。reduce()对数组正序操作;reduceRight()对数组逆序操作。let arr = [1, 2, 3, 4]; let sum = arr.reduce((accumulator, currentValue) => accumulator + currentValue, 0); console.log(sum); // 10
-
(5)
every() 和 some()
:some()只要有一个是true,便返回true;而every()只要有一个是false,便返回false。let arr = [1, 2, 3, 4]; let hasGreaterThanTwo = arr.some(value => value > 2); console.log(hasGreaterThanTwo); // true let arr = [1, 2, 3, 4]; let allGreaterThanZero = arr.every(value => value > 0); console.log(allGreaterThanZero); // true
-
(6)
find() 和 findIndex()
:find()返回的是第一个符合条件的值;findIndex()返回的是第一个返回条件的值的索引值。let arr = [1, 2, 3, 4]; let foundValue = arr.find(value => value > 2); console.log(foundValue); // 3 let arr = [1, 2, 3, 4]; let foundIndex = arr.findIndex(value => value > 2); console.log(foundIndex); // 2
-
(7)
for...of和for...in
:for...in用来遍历数组的索引(键名),for...of用来遍历数组的值。let arr = [1, 2, 3, 4]; for (let value of arr) { console.log(value); // 1, 2, 3, 4 } let arr = [1, 2, 3, 4]; for (let index in arr) { console.log(index); // 0, 1, 2, 3 console.log(arr[index]); // 1, 2, 3, 4 }
11、判断数组的方式
-
(1)
Array.isArray()
let arr = [1, 2, 3]; console.log(Array.isArray(arr)); // true let notArr = "Hello"; console.log(Array.isArray(notArr)); // false
-
(2)
instanceof Array
let arr = [1, 2, 3]; console.log(arr instanceof Array); // true let notArr = "Hello"; console.log(notArr instanceof Array); // false
-
(3)
Object.prototype.toString.call()
let arr = [1, 2, 3]; console.log(Object.prototype.toString.call(arr) === "[object Array]"); // true let notArr = "Hello"; console.log(Object.prototype.toString.call(notArr) === "[object Array]"); // false
-
(4)
constructor
属性`let arr = [1, 2, 3]; console.log(arr.constructor === Array); // true let notArr = "Hello"; console.log(notArr.constructor === Array); // false
-
(5)
Array.prototype.isPrototypeOf
:用于判断一个对象是否存在于另一个对象的原型链上,会检查该对象的原型链中是否有Array.prototype
。let arr = [1, 2, 3]; console.log(Array.prototype.isPrototypeOf(arr)); // true let obj = { a: 1 }; console.log(Array.prototype.isPrototypeOf(obj)); // false
12、数组去重
-
(1)
使用Set
let arr = [1, 2, 2, 3, 4, 4, 5]; let uniqueArr = [...new Set(arr)]; console.log(uniqueArr); // [1, 2, 3, 4, 5]
-
(2)
使用filter()和indexOf()
let arr = [1, 2, 2, 3, 4, 4, 5]; let uniqueArr = arr.filter((value, index) => arr.indexOf(value) === index); console.log(uniqueArr); // [1, 2, 3, 4, 5]
-
(3)
使用 reduce() 和 includes()
let arr = [1, 2, 2, 3, 4, 4, 5]; let uniqueArr = arr.reduce((acc, value) => { if (!acc.includes(value)) { acc.push(value); } return acc; }, []); console.log(uniqueArr); // [1, 2, 3, 4, 5]
-
(4)
使用 forEach() 和 Object
let arr = [1, 2, 2, 3, 4, 4, 5]; let obj = {}; let uniqueArr = []; arr.forEach(value => { if (!obj[value]) { obj[value] = true; uniqueArr.push(value); } }); console.log(uniqueArr); // [1, 2, 3, 4, 5]
-
(5)
使用Map
let arr = [1, 2, 2, 3, 4, 4, 5]; let uniqueArr = []; let map = new Map(); arr.forEach(value => { if (!map.has(value)) { map.set(value, true); uniqueArr.push(value); } }); console.log(uniqueArr); // [1, 2, 3, 4, 5]
-
(6)
使用for循环
let arr = [1, 2, 2, 3, 4, 4, 5]; let uniqueArr = []; for (let i = 0; i < arr.length; i++) { if (uniqueArr.indexOf(arr[i]) === -1) { uniqueArr.push(arr[i]); } } console.log(uniqueArr); // [1, 2, 3, 4, 5]
13、map和Object的区别
- (1)键的类型
Object
:键必须是字符串或符号类型(string
或 symbol
),如果你使用其他类型(如 number
或 object
),JavaScript 会自动将它们转换为字符串。
Map
: 键可以是任意类型,包括对象、函数、数字等。它不会自动转换键的类型。
let obj = {};
obj[1] = 'one'; // 1 会被转换为字符串 '1'
console.log(obj['1']); // 输出: "one"
let map = new Map();
map.set(1, 'one'); // 1 是一个 number,不会被转换
console.log(map.get(1)); // 输出: "one"
- (2)键的顺序
Object
:键的顺序通常是插入顺序,字符串类型的键会按插入顺序保留,数字类型的键则会自动按数值顺序排列。符号类型的键则保留插入顺序。
Map
保证所有键值对都保持插入顺序,无论键的类型如何。
let obj = { 'b': 2, 'a': 1, 1: 'one' };
console.log(Object.keys(obj)); // 输出: ["1", "b", "a"],数字键被排序到前面
let map = new Map();
map.set(1, 'one');
map.set('b', 2);
map.set('a', 1);
console.log([...map.keys()]); // 输出: [1, "b", "a"],保持插入顺序
- (3)原型链
Object
: 是通过原型链继承的,因此可能包含原型链上的属性,这会影响遍历(比如 toString
等内置方法会出现在原型链中)。
Map
: 不依赖于原型链,因此所有的键都是你自己定义的,没有额外的继承属性。
let obj = {};
console.log(obj.toString); // 输出: [Function: toString],来自原型链
let map = new Map();
console.log(map.toString); // 输出: [object Map],Map 不会从原型链继承属性
- (4)迭代
Object
: 没有内置的迭代器方法,所以不能使用for...of
循环进行迭代。可以使用 for...in
循环来遍历 Object
的可枚举属性名,然后再取值。也可以使用 Object.keys()
、Object.values()
、Object.entries()
。
Map
:是可迭代的,具有内置的迭代器方法,如 map.keys()
、map.values()
、map.entries()
,也可以直接使用 for...of
来遍历。
let obj = { a: 1, b: 2 };
for (let key in obj) {
console.log(key, obj[key]); // 输出: "a 1", "b 2"
}
let map = new Map([['a', 1], ['b', 2]]);
for (let [key, value] of map) {
console.log(key, value); // 输出: "a 1", "b 2"
}
- (5)大小的获取
Object
:没有内置的方法来直接获取键值对的数量。你需要使用 Object.keys()
、Object.values()
或 Object.entries()
,然后检查其数组的长度。
Map
:有内置的 size
属性,直接返回键值对的数量。
let obj = { a: 1, b: 2 };
console.log(Object.keys(obj).length); // 输出: 2
let map = new Map();
map.set('a', 1);
map.set('b', 2);
console.log(map.size); // 输出: 2
- (6)性能
Object
:在查找、插入、删除操作上通常有不错的性能表现,但由于其键需要强制转换为字符串,性能可能受到影响,尤其是处理大量数据时。
Map
:专门为键值对的存储和操作而设计,因此在频繁的增删查操作中,Map
的性能通常比 Object
更好,特别是处理大量数据时。
14、map和weakMap的区别
- (1)键类型
Map
: 的键可以是任何类型,包括对象、基本类型(如字符串、数字、布尔值等)。它允许存储非对象类型作为键。
WeakMap
: 的键只能是对象。它不能使用基本类型作为键,如字符串或数字。键必须是对象类型(包括数组、函数等)。
- (2)键的垃圾回收
Map
: 的键不会被垃圾回收,即使不再有引用指向这些键,键和值依然会保留在内存中,直到显式地删除它们。
WeakMap
:的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
- (3)迭代性
Map
: 支持标准的迭代操作,可以通过 for...of
或 map.keys()
、map.values()
、map.entries()
迭代键、值或键值对。
WeakMap
:不可迭代。你无法通过迭代访问它的键或值,因为这些键可能会在任何时候被垃圾回收。WeakMap
只提供了 get
、set
、delete
和 has
方法。
三、数据类型转换
1、如何进行隐式类型转换
隐式类型转换
是指在表达式或操作中,JavaScript 自动将一种数据类型转换为另一种数据类型,以满足语法或逻辑要求。隐式类型转换可以发生在算术、比较、逻辑运算等各种操作中。
原理
:通过ToPrimitive,来将值(无论是基本类型值还是对象)转换为基本类型值。如果值为基本类型,则直接返回值本身;如果值为对象怎么可以看第四条相关知识
。
- (1)算术运算符
当使用算术运算符(如 +
, -
, *
, /
, %
)时,JavaScript 会尝试将操作数转换为数字。
加法运算符 +
(字符串拼接) : 如果其中一个操作数是字符串
,JavaScript 会将另一个操作数转换为字符串,并进行拼接。
let num = 5;
let str = " apples";
console.log(num + str); // "5 apples"
其他算术运算符: 当使用 -
, *
, /
, %
等运算符时,JavaScript 会将操作数转换为数字,如果不能转为字符串,结果会是NaN。
let result = "5" - 2;
console.log(result); // 3 ("5" 被转换为数字 5)
let result = "hello" - 1;
console.log(result); // NaN
- (2)比较运算符
相等运算符 ==
: ==
进行比较时会尝试执行隐式类型转换,使两边的类型一致,然后进行比较。
console.log(1 == "1"); // true
// null 和 undefined 被认为相等
console.log(null == undefined); // true
// 布尔值在与其他类型比较时会转换为数字
console.log(true == 1); // true
console.log(false == 0); // true
大于/小于运算符 >
或 <
: 一般会将操作数转换为数字进行比较,如果无法转换,比较结果是 false
如果两边值都是字符串,会比较字母表顺序。
console.log("5" > 3); // true ("5" 转换为数字 5)
console.log("apple" > 3); // false (无法转换,比较失败)
'ca' < 'bd' // false,如果两边都是字符串,则比较字母表顺序
'a' < 'b' // true
- (3)逻辑运算符
逻辑运算符 &&
和 ||
会将操作数转换为布尔值。
注意
:
||
:如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。
&&
:相反,如果条件判断结果为 true 就返回第二个操作数的值,为 false 就返回第一个操作数的值。
console.log(0 && 1); // 0
console.log(1 && 2); // 2
console.log(0 || 1); // 1
console.log(null || "default"); // "default"
- (4)对象到原始类型的转换
当对象参与运算或比较时,JavaScript 会调用对象的 toString()
或 valueOf()
方法,将其转换为原始类型。但这两个方法有个先后执行顺序,通过ToPrimitive中的type来判断。
/**
* @obj 需要转换的对象
* @type 期望的结果类型
*/
ToPrimitive(obj,type)
当type
为number
时规则如下
第一步:调用`obj`的`valueOf`方法,如果为原始值,则返回,否则下一步;
第二步:调用`obj`的`toString`方法,如果为原始值,则返回,否则下一步;
第三步:抛出`TypeError` 异常。
当type
为string
时规则如下:
第一步:调用`obj`的`toString`方法,如果为原始值,则返回,否则下一步;
第二步:调用`obj`的`valueOf`方法,如果为原始值,则返回,否则下一步;
第三步:抛出`TypeError` 异常
注意
:如果对象为 Date 对象,则type
默认为string
;其他情况下,type
默认为number
。
var a = {}
a > 2 // false
a.valueOf() // {}, 上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步
a.toString() // "[object Object]",现在是一个字符串了
Number(a.toString()) // NaN,根据上面 < 和 > 操作符的规则,要转换成数字
NaN > 2 //false,得出比较结果
2、其他值到字符串的转换
(1)undefined
: 转换为字符串 "undefined"
。
(2)null
: 转换为字符串 "null"
。
(3)true
转换为字符串 "true"
,false
转换为字符串 "false"
。
(4)常规数字直接转换为相应的字符串,如 123
转换为 "123"
。负数也会转换为带符号的字符串,如 -456
转换为 "-456"
。特殊数字 NaN
和 Infinity
分别转换为 "NaN"
和 "Infinity"
。
(5)字符串类型在转换时保持不变,如 "hello"
仍是 "hello"
。
(6)对象最终需要通过调用 toString()
方法来进行字符串转换。如果对象没有自定义 toString
方法,默认会使用 Object.prototype.toString 返回 [object Object]
。
(7)数组通过调用toString()
方法来进行字符串转换,因为在Array.prototype上有重写的toString()
方法,数组的 toString()
方法的行为是将数组的每个元素转为字符串,并用逗号 ,
连接。
(8)函数: 转换为字符串时,函数的源代码会作为字符串返回。
(9)Symbol
:类型不能被自动转换为字符串。如果尝试将 Symbol
转换为字符串,会抛出错误。需要注意的是,Symbol
显式调用 String()
方法时,可以正确转换,但隐式转换(例如通过字符串拼接)会抛出错误。
3、其他值到数字类型值的转换
(1)如果字符串能够被解析为一个合法的数字,它将被转换为对应的数字值。如果字符串包含非数字字符(除了前后空格),无法完全解析为数字,结果是 NaN
。空字符串 ""
会被转换为数字 0
。
(2)true
被转换为 1
。false
被转换为 0
。
(3)null
被转换为数字 0
。undefined
被转换为 NaN
。
(4)对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。空数组 []
被转换为数字 0
。如果数组只有一个元素,该元素会被转换为数字。如果无法转换,结果是 NaN
。包含多个元素的数组无法直接转换为数字,结果是 NaN
。
console.log(Number([])); // 0
console.log(Number([123])); // 123
console.log(Number(["123"])); // 123
console.log(Number([null])); // 0
console.log(Number([undefined])); // NaN
console.log(Number([1, 2, 3])); // NaN
(5)Symbol
不能被隐式或显式地转换为数字。如果尝试将 Symbol
转换为数字,会抛出 TypeError
错误。
(6)BigInt
不能直接转换为 Number
类型,因为它可能超出 Number
的表示范围。如果需要将 BigInt
转换为 Number
,必须显式转换,并可能导致精度丢失。
4、其他值到布尔类型的值的转换
这个过程中,所有的值分为两类:truthy(真值) 和 falsy(假值)。假值的布尔强制类型转换结果为 false。真值转换为true
(1)假值
:false
本身、0
(数字零)、-0
(负零)、""
(空字符串)、null
、undefined
、NaN
(非数字)。
(2)真值
:true
本身非零数字(例如 1
, -1
, 3.14
等)、非空字符串(例如 "hello"
, " "
)、对象(包括数组、函数等,即使是空对象 {}
或空数组 []
也是 truthy)、Symbol
值、BigInt
值。
5、什么是包装类型
在 JavaScript 中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时 JavaScript 会在后台隐式地将基本类型的值转换为对象。
const a = "abc";
a.length; // 3 访问'abc'.length时,会将'abc'在后台转换成`String('abc')`,然后再访问其length属性
a.toUpperCase(); // "ABC"
使用valueOf
方法将包装类型倒转成基本类型
var a = 'abc'
var b = Object(a)
var c = b.valueOf() // 'abc'
最后
文章将js中数据类型相关的知识都串联了起来,方便记忆和理解,后续会不断将其它的知识都总结一下,期待和小伙伴们一起讨论。