js语法基础
变量声明
变量提升
暂时性死区
const
使用const声明的是一个常量,一旦声明就不能再重新赋值(注意:对于对象或数组类型的常量,虽然不能改变其引用地址,但是可以改变其内部属性或元素)。同样具有块级作用域,不存在变量提升,必须在声明的同时初始化
- 优先通过const声明变量,除非需要对该变量重新赋值
let
变量只在其声明的代码块(如{}内)中可见。let不存在变量提升,但存在暂时性死区(Temporal Dead Zone),即在变量声明之前的区域访问它会抛出错误ReferenceError
var
使用var声明的变量具有函数作用域或全局作用域,并且存在变量提升(hoisting)的现象。这意味着变量可以在声明之前就被使用,其值默认为undefined
数据类型
常见的数据类型有:string、number、boolean、null、undefined、bigInt、symbol、object8种,前七种为基本数据类型,object为引用数据类型
显式转换、隐式转换
显示转换
Number()
String()
toString()
Boolean()
parseInt()
parseFloat()
Symbol()
BigINt()
隐式转换
- 加号运算符
- 字符串+ 其他类型,其他类型会被转化为字符串
- 其他类型为symbol时,不转换会报错,
- 其他类型为对象时,会调用object对象的toString方法,如果返回的不是字符串,会报错
- 对象+ 其他类型,其他类型会转为字符串,对象会调用object对象的toString方法,如果返回的不是字符串,会报错
- 数值+其他类型(非字符串、对象)
- 数值 + 布尔值,布尔值true转化为1,false转化为0;
- 数值+ null,null转为0
- 数值+undefined,undefined转为NaN
- 数值+bigInt,会报错;
- 数值+对象,数值会转化为字符串,对象会调用object对象的toString方法,如果返回的不是字符串,会报错;
- 数值+Symbol,会报错
- 布尔值/null/undefined + 其他类型(非字符串、对象、number),布尔值/null/undefined会转为数值
- BigInt + 其他类型,会报错,因为其他类型会转为number,而bigInt不支持number的运算
- 减乘除
- 其他类型均会转为数值,特别是:
- 字符串:会转为数值,空字符串转为0,纯数值的字符串会被转为数值,非纯数值的字符串转为NaN
- 对象:会调用object对象的valueOf方法,如果返回的是数值或字符串,怎会将该数值或字符串(字符串会再转为数值)参与到运算中。如果对象没有valueOf方法或者该方法返回的仍是对象,会调用object对象的toString方法,如果返回的不是字符串,会报错。如果返回的是字符串,则会再次转为数值,参与到运算中那个
- bigInt,会报错
- symbol,会报错
- 逻辑运算符
逻辑与&&、逻辑或||、逻辑非!
其他数据类型会被转为布尔值,其中空字符串、null、undefined、NaN、0、-0、false、0n,会被转为false,其他会被转为true - 比较运算符
- ==和!=:比较时,会先将两边的值转为同一类型,再比较,如果两边类型不同,会报错
- =和!:比较时,不会进行转换
-
、>=、<、<=:比较时,会先将两边的值转为数值,再比较,
- NaN: NaN和任何值不相等,包括自身
- Symbol:Symbol和任何值不相等,包括自身
- 对象:会调用object对象的valueOf方法,如果返回的是数值或字符串,怎会将该数值或字符串(字符串会再转为数值)参与比较中中。如果对象没有valueOf方法或者该方法返回的仍是对象,会调用object对象的toString方法,如果返回的不是字符串,会报错。如果返回的是字符串,则会再次转为数值,参与比较中
流程控制
if
进入某个判断中后,就不会再进入其他判断
if (condition) {}
if (condition) {} else {}
if (condition) {} else if (condition) {} else {}
if (condition) {} else if (condition) {} else if (condition) {} else {}
for
- for…in: 遍历对象属性,包括继承的属性,因此如果想要遍历自身属性,需要使用hasOwnProperty方法
- for…of: 遍历可迭代对象自身的属性值,而不是key,包括数组、字符串、Set、Map等
for (let i = 0; i < 10; i++) {}
for(let i in is) {}
for(let i of is) {}
switch
- expression:要评估的表达式或变量。
- case:表示要匹配的值。
- break:可选的语句,用于在匹配后跳出switch语句。如果省略break,程序会继续执行下一个case的代码块,这通常被称为“case穿透”或“fall-through”。
- default:可选的语句,当没有case匹配时执行。
switch (expression) {
case value1:
// 当 expression 的值等于 value1 时执行的代码
break;
case value2:
// 当 expression 的值等于 value2 时执行的代码
break;
// ...
default:
// 当没有 case 匹配时执行的代码
}
while
while (condition) {}
do…while
- do…while 循环的 condition 是在循环体执行之后才被检查的。因此,即使 condition 的初始值为 false,循环体也会至少执行一次。
do {} while (condition){}
函数
函数分类
- 具名函数,有函数名称
function functionName(param1, param2, ...) {}
- 匿名函数,没有函数名称
- 匿名函数常用于变量赋值,回调函数。函数参数、对象属性值、立即执行函数、事件处理函数等
const a = function (param1, param2, ...) {}
- 箭头函数:
- 函数体只有一条语句,可以省略花括号和return关键字
- 箭头函数常用于函数参数、对象属性值、立即执行函数、事件处理函数等
const a = (param1, param2, ...) => {}
const a = (param1, param2, ...) => expression;
const a = (param1, param2, ...) => {
statements;
return expression;
}
面试题:
箭头函数的特点
箭头函数与普通函数的区别?
- 简洁的语法
- 可以省略 function 关键字,使用 => 来定义函数
- this 的绑定
- 箭头函数没有自己的 this 值。
- 箭头函数内部的 this 值始终是指向定义时所在的上下文,而不是调用时所在的上下文。
- 无 arguments 对象
- 箭头函数内部没有自己的 arguments 对象。
- 可以使用剩余参数(rest parameters)来替代
- 无 constructor 属性
- 箭头函数没有 constructor 属性。
- 试图访问箭头函数的 constructor 会导致 TypeError
- 不能作为构造函数使用
- 箭头函数不能使用 new 关键字来调用。
- 如果试图使用 new 关键字调用箭头函数,将会抛出一个 TypeError
- 不能作为 Generator 函数
- 箭头函数不能声明为 Generator 函数。
- 不能使用 * 符号来定义 Generator 箭头函数
参数传递
基础参数
默认参数
- 默认参数在基础参数之后,在…rest之前
- 函数调用时,如果某人参数没有传入,则会取其默认值
- rest参数不被统计到函数length中
function functionName(param1 = defaultValue1, param2 = defaultValue2, ...) {}
- 解构参数
当传入的参数是对象或者数组时,可以使用结构参数,函数内部可以直接使用其属性和元素
function functionName({param1, param2, ...}) {}
或者
function functionName([param1, param2, ...]) {}
- arguments参数
函数调用时,会自动传入一个arguments参数,该参数是一个类数组对象,包含所有传入的参数,可以通过arguments.length获取参数个数,arguments[0]获取第一个参数,arguments[1]获取第二个参数,arguments[arguments.length - 1]获取最后一个参数,arguments[arguments.length]获取最后一个参数,arguments[arguments.length + 1]获取最后一个参数,arguments[arguments.length - 1]获取最后一个参数
function sumUsingArguments() {
let total = 0;
for (let arg of arguments) {
total += arg;
}
return total;
}
console.log(sumUsingArguments(1, 2, 3, 4)); // 输出10
- rest参数
使用…操作符可以收集函数调用时多余的参数到一个数组中 - rest参数不被统计到函数length中
function sum(...numbers) {
return numbers.reduce((acc, curr) => acc + curr, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 输出15
作用域
闭包
由函数和其能够访问的所有外部变量(非全局变量)组成的特殊组合。当一个函数内部定义了另一个函数,并且内部函数访问了外部函数的变量时,就形成了闭包。内部函数能够记住其外部作用域的状态,即使外部函数已经执行完毕。
function outer() {
let outerVar = 1;
function inner() {
outerVar++
console.log(outerVar); // 内部函数可以访问外部函数的变量
}
return inner;
}
let closure = outer();
closure(); //1
closure(); //2
this指针
this 是一个特殊的对象,它在运行时根据上下文被自动赋值,代表当前执行上下文中函数的“所有者”或“调用者”
this指针的指向
this 的值取决于函数是如何被调用的。在 JavaScript 中,函数可以以多种方式调用,包括普通函数调用、作为对象的方法调用、构造函数调用、通过 call/apply/bind 方法调用等。每种调用方式都会影响 this 的值
- 函数调用:在普通函数调用中,this 通常指向全局对象(在浏览器中是 window,在 Node.js 中是 global)。在严格模式(strict mode)下,this 的值为 undefined。
- 对象方法调用:当函数作为对象的方法被调用时,this 指向调用该方法的对象。
- 构造函数调用:使用 new 关键字调用构造函数时,this 指向新创建的对象实例。
- 箭头函数:箭头函数没有自己的 this 值,而是从其封闭作用域继承 this 值。
- 事件处理程序:在事件处理程序中,this 通常指向触发事件的 DOM 元素。
- call、apply、bind 方法:这些方法可以显式地设置 this 的值
call、apply、bind
call
call方法:call 方法允许我们手动指定函数的 this 值,并调用函数。
function greet(greeting) {
console.log(`${greeting} ${this.name}`);
return `${greeting} ${this.name}`;
this.greeting = greeting;
this.sayHello = function () {
console.log(`${this.greeting} ${this.name}`);
return `${this.greeting} ${this.name}`;
}
}
let person = {
name: 'John',
greet: greeting => {}
}
person.greet('Hello');
person.greet.call({ name: 'Jane' }, 'Hi');
apply
apply方法:apply 方法允许我们手动指定函数的 this 值,并调用函数。与 call 方法不同的是,apply 方法接收一个数组作为参数。
function greet(greeting) {
console.log(`${greeting} ${this.name}`);
return `${greeting} ${this.name}`;
this.greeting = greeting;
this.sayHello = function () {
console.log(`${this.greeting} ${this.name}`);
return `${this.greeting} ${this.name}`;
}
}
let person = {
name: 'John',
greet: greeting => {}
sayHello: function () {
console.log(`${this.greeting} ${this.name}`);
return `${this.greeting} ${this.name}`;
}
}
person.greet('Hello');
person.greet.apply({ name: 'Jane' }, ['Hi']);
bind
bind方法:bind 方法允许我们创建一个新函数,该函数拥有一个指定的 this 值,并且参数列表包含一个或多个参数。
function greet(greeting) {
console.log(`${greeting} ${this.name}`);
return `${greeting} ${this.name}`;
this.greeting = greeting;
this.sayHello = function () {
console.log(`${this.greeting} ${this.name}`);
return `${this.greeting} ${this.name}`;
}
}
let person = {
name: 'John',
greet: greeting => {}
sayHello: function () {
console.log(`${this.greeting} ${this.name}`);
return `${this.greeting} ${this.name}`;
}
}
let sayHello = person.sayHello.bind({ name: 'Jane' });
sayHello();
手写bind方法
- 步骤:
- 创建一个新函数:这个新函数将作为 bind 的返回值。
- 设置 this 值:在新函数中,this 值应该指向 bind 的第一个参数。
- 处理传入的参数:允许传入额外的参数,并在新函数调用原始函数时传递这些参数。
- 处理原型链:确保新函数继承原始函数的原型,这样可以保留原始函数上的方法。
Function.prototype.myBind = function (thisArg) {
const context = thisArg || window;
const args = Array.from(arguments).slice(1);
const originalFunc = this;
return function bound(...remainingArgs) {
return originalFunc.apply(context, args.concat(remainingArgs));
};
};
函数的属性和方法
- length:函数的参数个数
- prototype:函数的原型对象,可以通过该对象添加属性和方法,使得所有该函数的实例都可以访问到该属性和方法
- call、apply、bind:调用函数,改变this指针,改变this指针后,函数的this指针指向新的对象
数组的操作
- 创建数组
- 字面量语法:let array = [element1, element2, …, elementN];
- 构造函数:let array = new Array(element1, element2, …, elementN);
- 访问与修改
- 索引访问:通过索引获取或设置元素,如 array[index]。
- length属性:获取数组长度,也可以通过修改length属性来截断或扩展数组
- push:向数组的末尾添加一个或多个元素,并返回新的长度
- pop:从数组的末尾移除一个元素,并返回被移除的元素
- unshift:向数组的开头添加一个或多个元素,并返回新的长度
- shift:从数组的开头移除一个元素,并返回被移除的元素
- splice:删除、替换或插入数组中的元素,并返回被删除的元素
- start:必需。从该位置开始修改数组。索引是从 0 开始的。
- deleteCount:可选。要删除的元素数量。如果不指定,则从 start 位置一直删除到数组末尾。0表示不删除
- items:可选。要插入到数组中的新元素。这些元素将从 start 位置开始插入。
array.splice(start, deleteCount, ...items)
- slice:返回数组的一部分作为新数组,不改变原数组,是原组的浅拷贝
- 开始索引(必需): 指定提取片段的起始位置。如果是负数,则表示从数组末尾开始计算的位置(-1 表示最后一个元素,-2 表示倒数第二个元素,依此类推)
- 结束索引(可选): 指定提取片段的结束位置。该位置不是包含在返回的数组中。如果未指定该参数,则默认为数组末尾。如果该参数大于数组的长度,则默认为数组的长度。如果该参数为负数,则表示从数组末尾开始计算的位置(-1 表示最后一个元素,-2 表示倒数第二个元素,依此类推)
let slicedArr = arr.slice(start, end);
- concat:返回一个数组,该数组包含原数组和所有附加数组的元素。
concat(arr1,arr2....)
- sort:对数组进行排序,默认按照升序排列。也可以自定义排序规则。返回-1,这第一个参数排在第二个参数前面,返回1,第二个参数排在第一个参数前面。返回0,两个参数相等,则顺序不变。
arr.sort((a, b) => a - b); // 升序
- reverse:反转数组,将数组中的元素顺序颠倒。
- join:将数组中的元素连接成一个字符串,并返回该字符串。
arr.join(分隔符)
- indexOf:返回数组中第一个与给定值相等的元素的索引,如果没有找到,则返回 -1。
- lastIndexOf:返回数组中最后一个与给定值相等的元素的索引,如果没有找到,则返回 -1。
- forEach:遍历数组中的每个元素,并执行一个函数。
- map:创建一个新数组,将数组中的每个元素都经过处理后返回。
- filter:创建一个新数组,将数组中的每个元素都经过处理后返回,并返回一个新数组。
- reduce:将数组中的元素通过一个函数进行累计计算,并返回计算结果。
- reduceRight:与 reduce 类似,但从数组的末尾开始计算。
- every:判断数组中的每个元素是否都满足给定的条件,并返回一个布尔值。
- some:判断数组中的至少一个元素是否满足给定的条件,并返回一个布尔值。
- find:返回数组中第一个满足给定条件的元素,如果没有找到,则返回 undefined。
- findIndex:返回数组中第一个满足给定条件的元素的索引,如果没有找到,则返回 -1。
- fill:用一个固定值填充数组中的所有元素,并返回该数组。
- includes:判断数组中是否包含某个元素,并返回一个布尔值。
- isArray:判断一个值是否为数组,并返回一个布尔值。
- Array.from:将一个类数组对象或可迭代对象转换为数组。
- Array.of:创建一个指定长度的数组,并将数组中的每个元素都设置为给定的值。
- Array.isArray:判断一个值是否为数组,并返回一个布尔值。
遍历数组
- for循环:通过索引访问数组中的每个元素。
- for…of循环:通过值访问数组中的每个元素。
- forEach:遍历数组中的每个元素,并执行一个函数。
forEach((element, index, array) => {
console.log(element, index, array);
// 可以在这里执行一些操作
// 可以在这里返回一个值,但通常不返回任何值
//todo
});
- map:创建一个新数组,将数组中的每个元素都经过处理后返回。
+ map((element, index, array) => {
// 可以在这里返回一个值,但通常不返回任何值
//todo
return element * 2; // 可以在这里执行一些操作,并将处理后的元素返回
};)
- filter:创建一个新数组,将数组中的每个元素都经过处理后返回,并返回一个新数组。
filter((element, index, array) => {
// 可以在这里返回一个值,但通常不返回任何值
return element > 5; // 可以在这里执行一些操作,并将处理后的元素返回
};)
- reduce:将数组中的元素通过一个函数进行累计计算,并返回计算结果。
reduce((accumulator, currentValue, currentIndex, array) => {
// 可以在这里返回一个值,但通常不返回任何值
return accumulator + currentValue; // 可以在这里执行一些操作,并将处理后的元素返回
}, initialValue);
对象的操作
创建对象
- 对象字面量:let obj = {property1: value1, property2: value2, …};
- 构造函数:let obj = new Object();
- 工厂函数
function createObject(property1, value1, property2, value2, ...) {return {property1: value1, property2: value2, ...};}
function createPerson(name, age) {
return {
name: name,
age: age
};
}
let person = createPerson('Bob', 25);
- 使用Object.create()方法(原型继承)
let personProto = {
sayHello: function() { console.log('Hello!'); }
};
let person = Object.create(personProto);
person.name = 'Charlie';
常见属性
- constructor:对象的构造函数,指向对象的构造函数。
- prototype:对象的原型对象,可以通过该对象添加属性和方法,使得所有该对象的实例都可以访问到该属性和方法。
- length:对象的属性个数,可以通过修改length属性来改变对象的属性个数。
常见方法
- toString:返回对象的字符串表示形式。
- toLocaleString:返回对象的本地字符串表示形式。
- valueOf:返回对象的原始值。
- hasOwnProperty:判断对象是否具有指定的属性,并返回一个布尔值。
- isPrototypeOf:判断一个对象是否是另一个对象的原型对象,并返回一个布尔值。
- propertyIsEnumerable:判断对象是否具有指定的可枚举属性,并返回一个布尔值。
- toJSON:返回对象的 JSON 表示形式。
- assign:Object.assign(targetObject, obj1, obj2…),将源对象的所有可枚举属性复制到目标对象中。
- 用于将一个或多个源对象的所有可枚举的自有属性的值复制到目标对象中。这个方法会返回目标对象。如果目标对象中的属性具有相同的键,则会被源对象中的属性值覆盖
- 执行的是浅拷贝,如果源对象中的属性值是对象,则只会复制对象的引用,而不会复制对象本身。
- Object.fromEntries():将一个键值对数组转换为一个对象。这个方法会返回一个对象,其中每个键值对都是数组中的元素。
访问与修改属性
- 属性访问:通过点符号或方括号符号访问对象的属性,如 obj.property1 或 obj[‘property1’]。
- 修改属性:通过点符号或方括号符号修改对象的属性,如 obj.property1 = newValue 或 obj[‘property1’] = newValue。
- 删除属性:通过 delete 关键字删除对象的属性,如 delete obj.property1。
遍历对象
- for…in:遍历对象的所有可枚举(非可迭代)属性,包括原型链上的属性。若要只获取可枚举属性,可以使用 Object.getOwnPropertyNames() 方法。
- Object.keys:返回对象的所有可枚举属性的数组。
- Object.values:返回对象的所有可枚举属性的值的数组。
- Object.entries:返回对象的所有可枚举属性的键值对的数组。
- Object.getOwnPropertyNames:返回对象的所有属性的数组,包括不可枚举属性。
- Object.getOwnPropertyDescriptor:返回指定属性的描述符对象。
- Object.defineProperty:定义对象的属性,包括可枚举性、可写性、可配置性等。
可枚举属性/不可枚举属性
- 对象的默认属性(直接在对象上定义的属性)是可枚举的,除非它们被特别定义为不可枚举。这意味着它们可以通过for…in循环或者Object.keys()方法被访问到
- 有些属性是不可枚举的,它们不会在for…in循环或者Object.keys()方法中被枚举出来。这些属性通常是内部使用的,或者由某些操作(如Object.defineProperty())特别定义为不可枚举的。
判断属性是否可枚举
可以使用propertyIsEnumerable()方法来检查一个对象的特定属性是否是可枚举的。这个方法会返回一个布尔值,指示指定的属性是否可枚举。
修改属性的可枚举性
Object.defineProperty(obj, prop, {
//enumerable定义可枚举性
enumerable: false
writable: true,
configurable: true
value: 100
get: function () {
return 100;
}
set: function (newValue) {
console.log('setter: ' + newValue);
this._firstName = newValue;
return newValue;
}
})
不可枚举属性的例子
- 对象的原型链上的属性:对象继承自其原型链的属性通常是不可枚举的。例如,所有对象都继承自Object.prototype,而Object.prototype上的方法(如toString()、valueOf()等)都是不可枚举的。
- 使用Object.defineProperty()定义的属性:当使用Object.defineProperty()方法定义属性时,可以通过将enumerable属性设置为false来将属性定义为不可枚举的。
- 某些内置对象的属性:JavaScript中的一些内置对象有一些特殊的属性,这些属性是不可枚举的。例如,数组的length属性是不可枚举的。
- getter和setter方法:如果通过Object.defineProperty()定义了一个getter或setter方法,并且没有同时定义一个可枚举的数据属性,那么这个getter或setter方法通常会被视为不可枚举的。
复制对象
浅拷贝
浅拷贝是指将对象或数组的第一层属性或元素复制到一个新的对象或数组中,但是如果属性值是对象或数组,只是复制了引用而非实际的对象或数组
主要方法
- Object.assign()
- 扩展运算符 (…)
- Array.prototype.slice() (针对数组)
- 手动遍历属性复制
- 变量赋值
深拷贝
将对象或数组的所有层级的属性或元素都复制到一个新的对象或数组中,包括嵌套的对象或数组,实现了完全独立的复制。
深拷贝可以通过递归遍历对象或数组的所有属性,并复制每个属性的值来实现。
由于深拷贝会复制所有层级的属性,因此可能会消耗更多的内存和时间。
例如,可以使用 JSON.parse(JSON.stringify(obj)) 来实现深拷贝,但需要注意该方法在处理包含函数、循环引用等特殊情况时可能会出现问题。
主要方法
- 手写
- 实现思路
- 通过递归遍历对象的属性,创建新对象并复制所有层级的属性
- 优点:能够处理各种类型的数据,包括函数和特殊对象。
- 缺点:
- 性能随着对象深度和复杂度增加而下降。
- 需要小心处理循环引用。
- 实现思路
function deepClone(target,cache = new Map()){
if(cache.get(target)){
return cache.get(target)
}
if(target instanceof Object){
let dist ;
if(target instanceof Array){
// 拷贝数组
dist = [];
}else if(target instanceof Function){
// 拷贝函数
dist = function () {
return target.call(this, ...arguments);
};
}else if(target instanceof RegExp){
// 拷贝正则表达式
dist = new RegExp(target.source,target.flags);
}else if(target instanceof Date){
dist = new Date(target);
}else{
// 拷贝普通对象
dist = {};
}
// 将属性和拷贝后的值作为一个map
cache.set(target, dist);
for(let key in target){
// 过滤掉原型身上的属性
if (target.hasOwnProperty(key)) {
dist[key] = deepClone(target[key], cache);
}
}
return dist;
}else{
return target;
}
}
- JSON.parse() 和 JSON.stringify()
- 实现思路: 将对象转换成JSON字符串,然后再将字符串解析成新的对象
- 缺点:
- 无法处理函数、RegExp、Date、undefined等特殊类型的属性,因为这些在序列化过程中会被忽略或转换。也不能拷贝
- 性能较差,特别是处理大对象时。
- 引起循环引用错误
- 对象不能是环状结构的,否则会导致报错
- 它不能保留原型链上的属性。
JSON.parse() 和 JSON.stringify()
const deepCopy = (obj) => {
return JSON.parse(JSON.stringify(obj));
};
- 使用第三方库
- 一些JavaScript库如lodash提供了_.cloneDeep()方法,可以方便地进行深拷贝
- 优点:
- 可以处理各种类型的属性,包括函数、RegExp、Date、undefined等。
- 可以处理循环引用的情况,不会出现死循环。
- 可以处理不可枚举的属性。
- 缺点:
- 需要引入外部依赖,增加项目体积。
- 在处理非常大的对象时可能影响性能
const _ = require('lodash');
const deepCopy = _.cloneDeep(originalObject);
- 使用递归
递归是一种常用的深拷贝方法,但需要考虑性能问题,如果对象非常大,可能会导致堆栈溢出。
function deepCopy(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
// 基本类型直接返回
// 如果是引用类型,则需要递归处理
return Array.isArray(obj) ? obj.map(item => deepCopy(item)) : Object.keys(obj).reduce((acc, key) => {
acc[key] = deepCopy(obj[key]);
return acc;
// 递归处理每个属性
return acc;
// 返回新的对象
return obj;
// 如果是引用类型,则需要递归处理
return Array.isArray(obj) ? obj.map(item => deepCopy(item)) : Object.keys(obj).reduce((acc, key) => {
acc[key] = deepCopy(obj[key]);
return acc;
// 递归处理每个属性
return obj;
}
}
}
}
- structuredClone()
- 一个在现代 Web 浏览器中提供的方法,用于深度克隆一个对象或数据结构,而不需要担心循环引用或不可克隆的值。它是在 Web Workers 之间安全地传输数据的一个重要工具,因为它可以确保克隆的数据是独立的副本,并且可以在不同的 JavaScript 执行上下文之间安全地传递。
- 缺点:
- 循环引用:如果对象中有循环引用,structuredClone() 会抛出错误。
- 不可克隆类型:
- DOM 元素:如 Element、Node、Window 等。
- 函数:函数是不可克隆的。
- 正则表达式:RegExp 对象也是不可克隆的。
- Error 对象:这些对象也是不可克隆的。
- 特殊值:如 undefined、null 以及某些特殊的符号(Symbol)
- 性能:对于非常大的数据结构,structuredClone() 可能会有较高的性能开销。
- 选择建议
- 选择深拷贝方法时,需要权衡项目的具体需求、对象的复杂度以及对性能的考量。
- 对于简单场景,JSON方法可能足够;
- 对于复杂对象或需要保持函数、特殊对象完整性的场景,则更适合使用递归拷贝或第三方库提供的深拷贝方法。
- 在现代前端开发中,如果环境支持,可以考虑使用structuredClone(),因为它提供了一种更加原生且健壮的深拷贝解决方案。
Map
Map 是一种键值对的集合,其中的键可以是任何类型的值,包括对象。Map 的键值对是唯一的,并且可以按照插入顺序访问。
Map 的键可以是任何类型的值,包括对象。
Map和对象的区别
- 键的类型
- Map 中的键可以是任何类型的值,包括对象。对象的键只能是字符串或 Symbol
- Map 中的键值对是唯一的,并且可以按照插入顺序访问。
- 迭代顺序
- 对象:迭代顺序不确定:在普通对象中,键的迭代顺序通常是不确定的,因为它基于对象的内部哈希表
- Map:迭代顺序是插入顺序:在 Map 中,键的迭代顺序是插入顺序,即键的插入顺序就是键的迭代顺序。
- 遍历方法
- 使用 for…in 循环或 Object.keys():可以使用 for…in 循环遍历对象的键,但需要注意它只会遍历可枚举的键,并且不保证顺序。
- 使用 forEach 或 for…of 循环:Map 提供了多种遍历方法,如 forEach 和 for…of,并且这些方法保证了顺序。
- 大小和性能
- 对象:大小未知:普通对象没有直接提供一个方法来获取键的数量,需要遍历所有键来计数
- Map:直接获取大小:Map 提供了 size 属性,可以直接获取键的数量
- 垃圾回收
- 对象:引用计数,对象中的键值对是通过引用计数管理的。如果一个键值对中有一个对象作为键,只要没有其他引用,垃圾回收器会释放它。
- Map:强引用,Map 中的键值对是强引用的,这意味着即使没有其他引用,垃圾回收器也不会释放这些键值对。
生成Map
new Map([iterable]):使用可迭代对象生成Map。
let map = new Map([
['name', 'Alice'],
['age', 30]
]);
Map常用属性
- size:返回Map中键值对的数量。
Map常用方法
- set:map.set(key, value),设置键值对。
- get:获取指定键的值。
let map = new Map([
['name', 'Alice'],
['age', 30]
]);
console.log(map.get('name')); // 'Alice'
console.log(map.get('height')); // undefined
- has:判断Map中是否存在指定键。
let map = new Map([
['name', 'Alice'],
['age', 30]
]);
console.log(map.has('name')); // true
console.log(map.has('height')); // false
- delete:删除指定键对应的键值对
let map = new Map([
['name', 'Alice'],
['age', 30]
]);
map.delete('name');
console.log(map); // Map(1) { 'age' => 30 }
- forEach:遍历Map中的键值对。
let map = new Map([
['name', 'Alice'],
['age', 30]
]);
map.forEach((value, key, map) => {
console.log(`Key: ${key}, Value: ${value}`);
});
// 输出:
// Key: name, Value: Alice
// Key: age, Value: 30
- clear:清空Map。
- entries:返回一个迭代器,迭代器返回键值对数组的迭代器。
let map = new Map([
['name', 'Alice'],
['age', 30]
]);
for (let entry of map.entries()) {
console.log(entry); // ['name', 'Alice'], ['age', 30]
}
- keys:返回一个迭代器,迭代器返回键的迭代器。
let map = new Map([
['name', 'Alice'],
['age', 30]
]);
for (let key of map.keys()) {
console.log(key); // 'name', 'age'
}
- values:返回一个迭代器,迭代器返回元素的迭代器。
let map = new Map([
['name', 'Alice'],
['age', 30]
]);
for (let value of map.values()) {
console.log(value); // 'Alice', 30
}
Set
生成Set对象
new Set([iterable]):使用可迭代对象生成Set。
Set常用属性
- size:返回Set中元素的数量。
Set常用方法
- add:添加元素。
- has:判断Set中是否存在指定元素。
- delete:删除指定元素。
- forEach:遍历Set中的元素。
- clear:清空Set。
- entries:返回一个迭代器,迭代器返回元素的迭代器。
- keys:返回一个迭代器,迭代器返回元素的迭代器。
- values:返回一个迭代器,迭代器返回元素的迭代器。
遍历方法
- forEach:遍历Set中的元素。
- for…of:遍历Set中的元素。
Set的用途
- 去重:Set可以确保元素不重复,因此可以用于去重。
- 集合运算:Set提供了多种集合运算的方法,如并集、交集、差集等,可以用于集合运算。
Set的实现原理
Set 的实现原理是使用一个 Map 来存储元素,其中 Map 的键是元素本身,值是 true。这样,Set 就可以确保元素不重复,并且可以通过 Map 的特性来快速查找元素是否存在。
计算交集、并集、子集
- 交集:使用两个 Set 对象,使用 forEach 方法遍历其中一个 Set 对象,判断另一个 Set 对象中是否存在相同的元素,如果存在,则将元素添加到结果 Set 对象中。
- 并集:使用 Set 来合并两个集合,并自动去除重复元素。
- 子集:遍历一个集合,并检查其所有元素是否都在另一个集合中。
字符串
字符串常用属性
- length:字符串的长度。
字符串常用方法
- charAt(index):返回指定索引位置的字符。
- concat(str):将当前字符串与另一个字符串连接成一个新的字符串。
- indexOf(str):返回指定子字符串在字符串中首次出现的索引位置。
- lastIndexOf(str):返回指定子字符串在字符串中最后一次出现的索引位置。
- slice(start, end):返回一个字符串的子字符串,从指定的开始索引到指定的结束索引(不包含结束索引)。
- substring(start, end):返回一个字符串的子字符串,从指定的开始索引到指定的结束索引(不包含结束索引)。
- toLowerCase():将字符串转换为小写字母。
- toUpperCase():将字符串转换为大写字母。
- trim():删除字符串两端的空格。
- replace(oldValue, newValue):将字符串中的旧值替换为新值。
- split(separator):将字符串按照指定的分隔符分割成多个子字符串,并返回一个包含子字符串的数组。
slice和subString的区别
- 参数处理:
- slice 支持负数索引,并且如果只有一个参数,它会从该位置截取到字符串末尾。
- substring 不支持负数索引,并且如果 start 大于 end,它会自动交换这两个参数的位置。
- 返回值:
- slice当 start 大于 end 时,slice 返回空字符串;而 substring 则会交换参数并返回正确的子串。
number
number常用属性
- Number.MAX_VALUE: 1.7976931348623157e+308
- Number.MIN_VALUE: 5e-324
- Number.NaN: NaN
- Number.NEGATIVE_INFINITY: -Infinity
- Number.POSITIVE_INFINITY: Infinity
number常用方法
- Number.isFinite(value): 判断是否为有限数
- Number.isInteger(value): 判断是否为整数
- Number.isNaN(value): 判断是否为 NaN
- Number.parseFloat(value): 将字符串转为浮点数
- Number.parseInt(value): 将字符串转为整数
- Number.toFixed(value): 将浮点数转为字符串,并保留指定位数的小数
- Number.toString(value): 将数字转为字符串
- Number.toExponential(value): 将数字转为科学计数法字符串
- Number.toPrecision(value): 将数字转为字符串,并保留指定位数
- Number.toLocaleString(value): 将数字转为本地字符串
说说Javascript数字精度丢失的问题,如何解决?
- 精度丢失的原因:
- 二进制表示的局限性:许多十进制小数无法精确表示为二进制小数。例如,0.1 在二进制中是一个无限循环小数。
- 浮点数的表示方式:浮点数由符号位、指数位和尾数位组成。由于尾数位的数量有限,某些小数无法精确表示。
- 解决方案
- 乘以 10 的幂:将小数转换为整数进行运算,然后再除以相应的 10 的幂次。适用于货币计算等场景。
- 使用 toFixed() 方法:将数字转换为字符串,并固定小数点后的位数。注意,此方法返回的是字符串。
- 使用 Number.EPSILON:Number.EPSILON 定义了两个相邻的浮点数之间的最小差值。可以用来比较两个浮点数是否接近。
- 使用第三方库:有一些库专门用于处理浮点数的精度问题,比如 decimal.js 或 bignumber.js。这些库提供了更高精度的算术运算
- 使用 BigInt:BigInt 类型可以用来表示任意大的整数。对于非常大的整数,可以使用 BigInt 来避免精度损失
- 使用 Math.round():如果只需要四舍五入到特定的小数位数,可以使用 Math.round() 方法。注意,这种方法可能会导致四舍五入的误差。
布尔值
布尔值常用方法
- Boolean.prototype.toString(): 返回布尔值的字符串表示
- Boolean.prototype.valueOf(): 返回布尔值的原始值
bigInt
生成bigInt
- BigInt(value): 将数字转为bigInt
- 数值后面加n,将数值转为bigInt
bigInt的常用方法
- BigInt.prototype.toString(): 将bigInt转为字符串
- BigInt.prototype.valueOf(): 返回bigInt的原始值
symbol
symbol常用方法
- Symbol.prototype.toString(): 将symbol转为字符串
- Symbol.prototype.valueOf(): 返回symbol的原始值
- Symbol.prototype.description: 返回symbol的描述
- Symbol.for(key): 获取或创建一个symbol
- Symbol.keyFor(sym): 获取symbol的key
- Symbol.iterator: 迭代器
symbol常用属性
- Symbol.hasInstance: 判断一个对象是否是某个类的实例
- Symbol.isConcatSpreadable: 是否展开数组
- Symbol.species: 创建子类
- Symbol.match: 匹配字符串
- Symbol.replace: 替换字符串
- Symbol.search: 搜索字符串
- Symbol.split: 分割字符串
undefined、null
- undefined: 未定义
- null: 空对象
undefined、null区别
- 比较运算符
- undefined == null: true
- undefined === null:false
- 含义
- undefined:通常表示一个变量已经声明但尚未赋值,或者一个函数没有返回任何值。它是一个全局变量,也是所有未定义类型的值。
- null 代表一个空的对象指针,通常用来表示尚未赋值的对象或表示“空”或“无”的值。null 是一个具体的值,可以被赋给变量
- typeof
- typeof undefined: undefined
- typeof null: object
- instanceof
- undefined、null均为原型,因此会报错
继承
原型、原型链
typeof与instanceof区别
- typeOf
- 作用:用于检测一个变量或表达式的类型。它返回一个字符串,表示变量或表达式的类型
- 返回的类型:
- ‘number’
- ‘bigint’
- ‘string’
- ‘boolean’
- ‘undefined’
- ‘symbol’
- ‘object’ (null 也会返回 ‘object’)
- ‘function’
- instanceOf
- 作用:instanceof 是一个二元运算符,用于测试一个对象是否为某个构造函数的实例。
- 返回值:它返回一个布尔值,表示对象是否属于指定的类或构造函数
- 其他区别
- 处理 null 和 undefined:
- typeof null 返回 ‘object’(这是一个特殊情况,尽管 null 不是对象)。typeof undefined 返回 ‘undefined’。
- instanceof 无法正确处理 null 和 undefined,因为它们不是构造函数的实例
- 对象检测:
- typeof 对于所有的对象(除了 null、function)都会返回 ‘object’。
- instanceof 可以用来区分不同类型的对象实例。
- 使用场景:
- 使用 typeof 来检测基本类型或简单类型检查。
- 使用 instanceof 来检测复杂的对象实例关系、
- 处理 null 和 undefined:
原型
原型用于实现对象之间的继承。每个 JavaScript 对象都有一个内部属性,称为 [[Prototype]],它引用了另一个对象。这个被引用的对象就是该对象的原型
在 JavaScript 中,几乎所有的对象都会关联到一个原型对象(除了 null 和通过 Object.create(null) 创建的对象外)。这个原型对象包含了可以被其关联对象共享的属性和方法。
原型常用的方法
- Object.getPrototypeOf(obj):返回 obj 的原型对象。
- Object.setPrototypeOf(obj, prototype):设置 obj 的原型对象为 prototype。
- Object.create(prototype):创建一个新对象,其原型对象为 prototype。
原型链上的方法
- toString(), valueOf(): 这些是所有JavaScript对象从Object.prototype继承的基本方法,用于对象的字符串表示和值表示。
- hasOwnProperty(prop): 检查对象自身属性中是否具有指定的属性,不检查原型链。
- isPrototypeOf(object): 检查一个对象是否在另一个对象的原型链上。
- propertyIsEnumerable(prop): 检查对象自身的某个属性是否可枚举
获取原型
- proto:这是每个 JavaScript 对象的内置属性,它指向该对象的原型对象。例如,let obj = { name: ‘John’ };obj.proto === Object.prototype。
原型链
- 依据:obj.proto === Obj.prototype
实例的原型等于其构造函数的原型对象
原型链是由一系列的原型对象连接起来的链式结构。当一个对象访问一个属性或方法时,如果该对象本身不存在该属性或方法,那么它会查找其原型对象上是否存在该属性或方法。如果原型对象上也不存在,则会继续查找原型的原型,以此类推,直到找到该属性或方法或到达原型链的末端(null)。这个过程称为原型链查找
说说new操作符具体干了什么?
new 操作符用于创建一个由构造函数定义的新对象
- 创建新对象,绑定原型:
- new 操作符首先创建一个新的空对象。
- 这个新对象的原型会设置为构造函数的 prototype 属性所指向的对象。
- 绑定 this:
- 将新创建的对象作为 this 的值绑定到构造函数中。
- 这意味着在构造函数内部访问的 this 实际上就是新创建的对象。
- 执行构造函数:
- 执行构造函数的代码,使用新创建的对象作为上下文。
- 构造函数可以使用 this 来设置新对象的属性或方法。
- 返回新对象:
- 如果构造函数显式返回一个对象,则返回该对象。
- 如果构造函数返回一个非对象值(如 undefined、null、number、string、boolean 等),则忽略返回值,直接返回新创建的对象
类
类(Class)是ES6(ECMAScript 2015)引入的一个重要特性,它提供了一种面向对象的编程风格,使得定义和创建对象变得更加直观和易于理解。尽管JavaScript的类实质上是基于原型继承的语法糖,但它们引入了一种更接近传统面向对象语言的语法
定义类
class MyClass {
constructor(parameters) {
// 初始化属性和执行其他设置
}
method1() {
// 方法定义
}
static staticMethod() {
// 静态方法,不需要实例化即可调用
}
}
- 使用class关键字来定义一个类,类名通常首字母大写,遵循驼峰命名法
- 构造函数:类中可以定义一个特殊的constructor方法,它是类的初始化方法,用于创建类的实例时自动调用。构造函数可以接受参数,用于初始化实例的属性
- 方法:类中可以定义多个方法,用于实现类的功能
- 静态方法:静态方法不需要实例化,可以直接通过类名调用,用于实现类的通用功能,例如创建一个工具类,其中包含多个静态方法,用于实现通用的功能
- 私有属性/私有方法
- ES2022引入了私有字段的概念,通过在属性名或方法名前加#符号来声明私有属性/方法,例如#privateField。私有属性/方法只能在类的内部访问
class MyClass {
#privateField;
constructor(value) {
this.#privateField = value;
}
getPrivateField() {
return this.#privateField;
}
}
类继承
- ES6引入了类继承的概念,通过extends关键字可以继承父类的属性和方法,子类可以通过super关键字调用父类的构造函数和父类方法
class ParentClass {
constructor() {
// ...
}
// ...
}
class ChildClass extends ParentClass {
constructor() {
//在子类中,super关键字用于调用父类的构造函数或访问父类的属性和方法
super();
// ...
}
// ...
}
实例化
- ES6引入了实例化概念,通过new关键字可以创建类的实例,实例化后的对象可以通过this关键字访问类的属性和方法
const myInstance = new MyClass();
拓展运算符
ES6中的一种语法,用于展开可迭代对象(如数组、字符串或类数组对象)或对象字面量。它可以将可迭代对象的元素或对象的属性展开,以便在需要多个参数或元素的地方使用
数组的展开
使用拓展运算符可以将一个数组展开为多个独立的元素。
const arr = [1, 2, 3];
console.log(...arr); // 输出:1 2 3
字符串的展开
使用拓展运算符可以将一个字符串展开为单个字符
const str = 'Hello';
console.log(...str); // 输出:H e l l o
对象的展开
使用拓展运算符可以将一个对象的属性展开到另一个对象中
const obj1 = { x: 1, y: 2 };
const obj2 = { ...obj1, z: 3 };
console.log(obj2); // 输出:{ x: 1, y: 2, z: 3 }
函数调用时的参数传递
使用拓展运算符可以将一个数组的元素作为函数的参数传递
function sum(a, b, c) {
return a + b + c;
}
const numbers = [1, 2, 3];
console.log(sum(...numbers)); // 输出:6
解构
从ES6(ECMAScript 2015)开始引入的一种特性,它提供了一种优雅而简洁的方式来提取数组或者对象中的值,直接赋给对应的变量
数组解构赋值
数组解构赋值允许我们通过模式匹配的方式从数组中提取值并赋给变量
const numbers = [1, 2, 3];
const [a, b, c] = numbers;
console.log(a); // 输出:1
console.log(b); // 输出:2
console.log(c); // 输出:3
对象解构赋值
对象解构允许你将对象的属性值直接赋给同名的变量
const person = { name: 'John', age: 30 };
const { name, age } = person;
console.log(name); // 输出:John
console.log(age); // 输出:30
嵌套解构
可以用于多层嵌套的数组或对象
let nested = [[1, 2], [3, 4]];
let [[a, b], [c, d]] = nested;
// a = 1, b = 2, c = 3, d = 4
let { obj: { prop } } = { obj: { prop: 'value' } };
// prop = 'value'
默认值
解构赋值允许设置默认值,当解构的值为 undefined 或 null 时,会使用默认值
const numbers = [1, 2];
const [a, b, c = 3] = numbers;
console.log(a); // 输出:1
console.log(b); // 输出:2
console.log(c); // 输出:3
忽略某些值/属性
可以用 , 间隔来跳过某些值或属性
let [first, , third] = [1, 2, 3]; // first = 1, third = 3
let { foo, , baz } = { foo: 'hello', bar: 'ignore', baz: 'world' }; // foo = 'hello', baz = 'world'
JSON
JSON支持六种数据类型:
- 数字(Number):整数或浮点数。
- 字符串(String):必须使用双引号包围。
- 布尔值(Boolean):true 或 false。
- 数组(Array):值的有序集合,在方括号[]中。
- 空值(Null):表示一个空对象或者不存在的值。
- 对象(Object):键值对的集合,在花括号{}中。
- 数组:可以包含任何类型的值,包括对象和其它数组
- 对象:键必须是字符串,值可以是上述任何类型,包括另一个对象或数组。
- 不支持undifined、Date、Function、NaN、Infinity等,这些值在转换为JSON时会被忽略或替换。不支持循环引用
JSON字符串与JavaScript对象的转换
- JSON.stringify():将JavaScript值转换为JSON字符串。
var obj = {name: "Alice", age: 30};
var jsonString = JSON.stringify(obj);
- JSON.parse():将JSON字符串转换为JavaScript值。
var jsonString = '{"name": "Alice", "age": 30}';
var obj = JSON.parse(jsonString);
异步
宏任务和微任务
宏任务:
setTimeout
setInterval
setImmediate
requestAnimationFrame
微任务:
process.nextTick
MutationObserver
Promise.then catch finally