前端杂学录(一)

真的不想学习啦 2024-10-02 13:03:01 阅读 69

记录看到的前端知识

1.<code>const、letvar 是 JavaScript 中用于声明变量的三种方式,它们之间有一些重要的区别和用法。

1. var

作用域var 声明的变量是函数作用域或全局作用域。在块级作用域(如 iffor 等)中声明的 var 变量在外部仍然可访问。提升var 变量会被提升到作用域的顶部,但值是 undefined,直到赋值语句执行。

console.log(x); // 输出: undefined

var x = 5;

console.log(x); // 输出: 5

if (true) {

var y = 10;

}

console.log(y); // 输出: 10 (y 在外部可访问)

2. let

作用域let 声明的变量是块级作用域,只在其所在的块内可访问。提升let 变量也会被提升,但在声明之前无法访问,会导致 ReferenceError

// console.log(z); // 会报错: Cannot access 'z' before initialization

let z = 5;

console.log(z); // 输出: 5

if (true) {

let a = 10;

console.log(a); // 输出: 10

}

// console.log(a); // 会报错: a is not defined

 3. const

作用域const 的作用域与 let 相同,也是块级作用域。赋值const 用于声明常量,必须在声明时初始化,并且一旦赋值后不能被重新赋值。对象和数组的内容可以修改,但引用不能改变。

const b = 5;

// b = 10; // 会报错: Assignment to constant variable.

const obj = { name: 'Alice' };

obj.name = 'Bob'; // 允许修改对象的属性

console.log(obj.name); // 输出: Bob

// const arr = [1, 2, 3];

// arr = [4, 5, 6]; // 会报错: Assignment to constant variable.

// arr.push(4); // 允许修改数组内容

console.log(arr); // 输出: [1, 2, 3, 4]

2.  continue用于跳过本次循环,执行下一次循序。break用于彻底退出整个循环 

3.JavaScript规定了几种语言类型

原始类型(Primitive Types)

Undefined:一个未定义的值。Null:表示“无”或“空”值。Boolean:布尔值,只有两个取值:truefalseNumber:数字类型,包括整数和浮点数。BigInt:用于表示大于 Number.MAX_SAFE_INTEGER 的整数。String:字符串类型,用于表示文本。Symbol:用于创建独一无二的标识符。

引用类型(Reference Types)

Object:用于表示对象,包括数组、函数、日期等。

4.js中,数组的那个方法可以得到数组的元素和下标

在 JavaScript 中,Array.prototype.forEach() 方法可以同时获取数组的元素和下标。它接受一个回调函数,回调函数有三个参数:元素值(value)、索引(index)和整个数组(array)。

也可以使用 Array.prototype.map()Array.prototype.entries() 方法来获得元素和下标。entries() 方法返回一个包含数组键值对的迭代器

5.JavaScript对象的底层数据结构是什么

JavaScript 对象的底层数据结构是基于**哈希表(Hash Table)**的实现。对象在 JavaScript 中是一种键值对集合,它的属性(键)可以是字符串或 Symbol,而属性的值可以是任意类型的数据。

详细解释:

键值对存储: JavaScript 对象的每个属性都由键(Key)和对应的值(Value)组成。键通常是字符串(也可以是 Symbol),值可以是任何数据类型。在对象内部,键会被转换为某种唯一标识符,以方便快速查找。

哈希表结构: 对象的底层采用了类似哈希表的数据结构。当我们向对象添加键值对时,JavaScript 引擎会通过一个哈希函数将键映射到对象的内部存储位置。通过这种方式,JavaScript 可以在常数时间复杂度内(O(1))完成对对象属性的查找、添加和删除。

属性查找: 当访问对象属性时,JavaScript 引擎会使用哈希函数来计算键对应的位置,然后直接查找到该属性的值。这种查找过程非常高效。

隐藏类和属性优化(JIT优化): 虽然 JavaScript 对象的基本实现是基于哈希表,但现代 JavaScript 引擎(如 V8 引擎)使用了一些优化技术,提升对象访问性能。例如,JavaScript 引擎可能会为对象创建隐藏类(Hidden Class)或转为类数组结构来加速常用对象的访问。对于对象的不同属性组合,JavaScript 引擎可能会动态生成不同的类结构,以加速相同结构的对象的处理。

对象存储的扩展:

普通对象(Plain Object):使用哈希表存储键值对,键为字符串或 Symbol数组(Array):数组实际上也是一种对象,但它的键是数字索引,底层可能会采用不同的优化方式(如稀疏数组处理)。MapSet:这两种数据结构也是基于哈希表实现的,但它们比普通对象支持更多的数据类型作为键。

6.JavaScript 中的 MapSet

JavaScript 提供了 MapSet,它们是高级的数据结构,内部使用哈希表来实现,允许高效地存储、查找和删除数据。

Map

Map 是一个键值对的集合,它允许任何类型的键(包括对象、数组等),与传统对象不同,Map 的键不仅限于字符串或 Symbol 类型。

用法:

创建一个 Map

const map = new Map();

添加键值对

map.set('name', 'Alice'); // 键是字符串

map.set(1, 'Number 1'); // 键是数字

map.set({id: 2}, 'Object'); // 键是对象

获取值

console.log(map.get('name')); // 输出: Alice

删除键值对

map.delete('name');

检查键是否存在

console.log(map.has(1)); // 输出: true

遍历 Map

map.forEach((value, key) => {

console.log(key, value);

});

// 或者用 for...of

for (const [key, value] of map) {

console.log(key, value);

}

Map 的特性

保留键值对插入的顺序。键可以是任何类型,而不仅限于字符串。

Set

Set 是一个存储唯一值的集合,类似于数组,但不允许重复的元素。Set 内部也是通过哈希表实现的,因此查找和删除操作效率很高。

用法:

创建一个 Set

const set = new Set();

添加值

set.add(1);

set.add(5);

set.add(1); // 重复的值会被忽略

检查值是否存在

console.log(set.has(1)); // 输出: true

console.log(set.has(3)); // 输出: false

删除值

set.delete(5);

遍历 Set

set.forEach(value => {

console.log(value);

});

// 或者用 for...of

for (const value of set) {

console.log(value);

}

Set 转换为数组

const array = [...set]; // 使用扩展运算符

Set 的特性

值是唯一的,不能重复。不保证元素插入的顺序(ES6 规范保证了顺序的保留,但旧版本浏览器可能不支持)

对比 Map 和普通对象

特性 Map 普通对象 (Object)
键类型 任意类型(对象、函数、基本类型) 只能是字符串或 Symbol
键的顺序 保留插入顺序 无保证(但大部分浏览器保留顺序)
高效性 通过哈希表实现,查找和删除速度快 查找和删除速度稍慢
可迭代性 原生支持迭代(for...of 不支持原生迭代,需手动处理

7.js中还有那些方法的参数是固定的

1. 数组方法(Array Methods)

forEach()

参数顺序: value, index, array用于遍历数组中的每一个元素。

map()

参数顺序: value, index, array用于对数组中的每个元素执行函数并返回新数组。

filter()

参数顺序: value, index, array用于筛选数组中的元素,返回满足条件的元素组成的新数组。

reduce()

参数顺序: accumulator, currentValue, currentIndex, array用于将数组中的元素汇总为单个值。

some() / every()

参数顺序: value, index, arraysome():检查数组中是否有任意元素满足条件。every():检查数组中是否所有元素都满足条件。

find() / findIndex()

参数顺序: value, index, arrayfind():返回第一个满足条件的元素。findIndex():返回第一个满足条件的元素的索引。

2. Object 方法

Object.entries()

返回[key, value] 数组的迭代器。将对象的键值对转换为数组,每个元素是 [key, value]

Object.keys()

返回:对象的键组成的数组

Object.values()

返回:对象的值组成的数组。

3. MapSet 方法

map.forEach()

参数顺序: value, key, map遍历 Map 中的每个键值对。

set.forEach()

参数顺序: value, valueAgain, set遍历 Set 中的每个元素,valuevalueAgain 是相同的,因为 Set 没有键,只有值。

4. Promise 方法

Promise.then()

参数顺序: onFulfilled, onRejected用于处理 Promise 成功和失败的回调。

Promise.catch()

参数顺序: onRejected用于处理 Promise 的失败回调。

Promise.finally()

参数顺序: callback无论 Promise 是成功还是失败,都会调用 finally 的回调。

5. 事件监听器(Event Listener)

addEventListener()

参数顺序: event, callback, options用于为 DOM 元素添加事件监听器。

6. 定时器方法

setTimeout()

参数顺序: callback, delay, ...args在指定的延迟后执行回调函数。

 setInterval()

参数顺序: callback, delay, ...args以固定的时间间隔重复执行回调函数。

 8.手动实现一个简单的 Symbol

let idCounter = 0;

function MySymbol(description) {

const uniqueId = `@@Symbol(${description})_${idCounter++}`;

return uniqueId;

}

const sym1 = MySymbol('test');

const sym2 = MySymbol('test');

console.log(sym1 === sym2); // false, 它们是不同的唯一标识符

console.log(sym1); // @@Symbol(test)_0

console.log(sym2); // @@Symbol(test)_1

分析:

唯一性:通过维护一个计数器 idCounter,每次调用 MySymbol() 时都会返回一个不同的唯一字符串,即使描述符相同也不会重复。描述符:我们可以为每个 Symbol 提供一个可选的描述符,类似于 JavaScript 的 Symbol

9.JavaScript中的变量在内存中的具体存储形式

1. 内存的基本结构

JavaScript 的内存模型可以分为两个区域:

栈内存(Stack):用于存储简单的、大小固定的数据(如原始类型和函数调用的上下文)。堆内存(Heap):用于存储复杂的数据(如对象、数组),这些数据大小不固定,会动态分配内存。

2. 原始类型的存储

原始类型(Primitive Types)的数据直接存储在栈内存中。它们包括:

Number(数字)String(字符串)Boolean(布尔)Null(空值)Undefined(未定义)Symbol(符号)BigInt(大整数)

栈内存是一种高效的内存结构,分配和释放速度非常快。对于这些原始类型,变量直接保存它们的值

特点:

存储在栈内存中的原始值是不可变的(尤其是 String 类型,虽然它表现为可以改变,但每次操作都会返回一个新的字符串)。变量直接保存数据的值,访问时直接返回值,操作非常快速。

3. 引用类型的存储

引用类型(Reference Types)包括:

Object(对象)Array(数组)Function(函数)其他如 DateRegExp 等复杂数据类型

这些数据类型的值在堆内存中存储。变量本身并不直接存储值,而是存储一个引用,这个引用指向存储在堆内存中的数据。

特点:

引用类型的数据存储在堆中,变量保存的是对堆内存的引用。访问对象、数组等引用类型时,实际上是通过栈中的引用去查找堆中的数据。当多个变量引用同一个对象时,它们指向的都是堆内存中的同一块地址。

4. 内存分配与垃圾回收

JavaScript 是一种自动管理内存的语言,这意味着开发者不需要手动分配或释放内存。JavaScript 引擎使用垃圾回收机制来管理内存的释放。

内存分配

原始类型:直接在栈中分配空间,因为它们的大小是固定的,存取速度也很快。引用类型:需要在堆中分配内存,堆内存的分配是动态的,适合存储复杂和大小不固定的数据结构。

垃圾回收

JavaScript 的垃圾回收机制(如常用的标记清除法)会定期检查哪些对象不再被引用,然后释放其占用的内存。

当一个变量不再引用任何堆内存中的对象时,该对象会被标记为“可回收”。当垃圾回收器运行时,它会清理这些不再使用的对象,释放内存。

5. 原始类型与引用类型的区别

特性 原始类型 引用类型
存储位置 栈内存 堆内存
变量存储的内容 直接存储值 存储堆内存地址的引用
值的大小 固定(相对较小) 动态(大小可变)
拷贝行为 复制值 复制引用
访问速度 较慢

6. 示例:拷贝行为

原始类型的拷贝:

原始类型的变量是值的拷贝,两个变量之间不会相互影响。

引用类型的拷贝:

引用类型的变量是引用的拷贝,两个变量指向同一个对象,修改其中一个会影响另一个。

7. 内存泄漏

尽管 JavaScript 有垃圾回收机制,但某些情况下可能会出现内存泄漏。常见的内存泄漏原因包括:

全局变量:全局变量不会被垃圾回收器回收,可能会导致内存长期占用。闭包:如果闭包中保存了对外部变量的引用,可能会导致这些变量无法被释放。DOM 引用:删除 DOM 节点时,如果仍然有 JavaScript 引用指向该节点,它就不会被回收。

总结

原始类型:存储在栈内存中,值是直接存储的,数据大小固定,访问速度快。引用类型:存储在堆内存中,变量保存的是对数据的引用,数据大小动态变化,适合复杂的数据结构。垃圾回收:JavaScript 自动管理内存,使用垃圾回收器清理不再使用的对象,以防止内存泄漏。

10.基本类型对应的内置对象,以及他们之间的装箱拆箱操作

1. 基本类型与对应的内置对象

JavaScript 中有 6 种基本类型,每种基本类型都有其对应的内置对象:

基本类型 对应的内置对象
String String
Number Number
Boolean Boolean
Symbol Symbol
BigInt BigInt
Null
Undefined
说明:

NullUndefined 没有对应的内置对象,因此它们不能通过装箱操作转化为对象。其他基本类型都有内置对象,允许我们调用某些方法,如 StringlengthNumbertoFixed() 等。

2. 装箱(Boxing)

装箱是指将一个基本类型转换为其对应的对象类型。这是 JavaScript 引擎在幕后自动进行的,因此开发者不需要显式地进行转换。

例子:自动装箱

当你对一个基本类型调用方法时,JavaScript 引擎会自动将基本类型“装箱”为其对应的包装对象:

const str = "hello";

console.log(str.length); // 5

在这段代码中,str 是一个基本的 String 类型,但我们调用了 str.length。JavaScript 在幕后自动将 str 装箱为 new String("hello"),然后在这个包装对象上查找 length 属性。

装箱过程大致如下:

const str = "hello";

const tempStrObj = new String(str); // 临时将基本类型转换为对象

console.log(tempStrObj.length); // 获取 length 属性

tempStrObj = null; // 然后销毁临时对象

同样的,数字和布尔值也会在调用方法时自动装箱:

const num = 42;

console.log(num.toFixed(2)); // 输出: "42.00"

const bool = true;

console.log(bool.toString()); // 输出: "true"

3. 拆箱(Unboxing)

拆箱是指将一个包装对象转换为它的基本类型值。当你将对象用于需要基本类型的上下文中时,JavaScript 会自动执行拆箱操作。

例子:自动拆箱

const numObj = new Number(123);

console.log(numObj + 1); // 124

在这段代码中,numObj 是一个 Number 对象,但当它与 1 相加时,JavaScript 自动将其拆箱为基本的 Number 类型,即 123,然后进行加法运算。

4. 手动装箱和拆箱

虽然 JavaScript 会自动进行装箱和拆箱,但你也可以手动创建包装对象。

手动装箱

你可以通过使用内置对象的构造函数手动创建包装对象:

const strObj = new String("hello");

const numObj = new Number(123);

const boolObj = new Boolean(false);

console.log(typeof strObj); // "object"

console.log(typeof numObj); // "object"

console.log(typeof boolObj); // "object"

需要注意的是,手动创建的包装对象是对象类型,而不是基本类型。

手动拆箱

包装对象可以通过 .valueOf() 方法或隐式上下文被拆箱为基本类型:

const strObj = new String("hello");

const numObj = new Number(123);

console.log(strObj.valueOf()); // "hello"(拆箱为基本类型字符串)

console.log(numObj.valueOf()); // 123(拆箱为基本类型数字)

console.log(typeof strObj.valueOf()); // "string"

console.log(typeof numObj.valueOf()); // "number"

5. 装箱与拆箱的应用场景

装箱和拆箱通常是在 JavaScript 引擎内部自动处理的,开发者通常不需要手动处理这些转换。但是理解它们在以下场景中很有帮助:

基本类型方法调用:基本类型不能直接调用方法,但 JavaScript 会自动装箱以允许你这样做,例如字符串的 .length 属性或数字的 .toFixed() 方法。

操作包装对象:如果你手动创建了包装对象(如 new String()),要小心它的行为与基本类型不同。例如,比较对象和基本类型时可能产生意外结果。

6. 包装对象的陷阱

使用包装对象时,有一些潜在的陷阱需要注意:

1. new Boolean() 的问题

当你使用 new Boolean(false) 创建一个 Boolean 对象时,尽管值是 false,对象的本质依然为 true,因为对象在 JavaScript 中总是被视为 true

2. 引用比较

包装对象和基本类型在比较时会出现不同的行为。包装对象是引用类型,因此它们的比较会检查是否是相同的引用,而不是值相同。

7. 总结

装箱(Boxing):将基本类型转换为包装对象,以便能够像对象一样调用方法(如 toString()length)。这个过程通常是自动的。拆箱(Unboxing):将包装对象转换回基本类型。通常在需要基本类型的上下文中自动发生(如运算操作或显式调用 .valueOf())。避免手动创建包装对象:一般情况下,你不需要手动使用包装对象构造函数(如 new Number()new String()),因为它们可能会导致混淆和不必要的复杂性。

11.理解值类型和引用类型

1. 值类型(Primitive Types)

值类型是指简单的数据类型,直接存储其值。这些类型的特点是:

存储位置:值类型的变量直接在栈内存中存储数据。不可变性:值类型的数据是不可变的,任何对其的修改都会生成一个新的值,而不是改变原来的值。比较行为:当比较两个值类型时,会比较它们的实际值。

常见的值类型

Number:数字类型(如 13.14String:字符串类型(如 "hello"Boolean:布尔类型(truefalseNull:表示空值Undefined:表示未定义的值Symbol:唯一的标识符BigInt:表示大整数

2. 引用类型(Reference Types)

引用类型是指复杂的数据类型,存储的是对内存中实际数据的引用。这些类型的特点是:

存储位置:引用类型的变量在栈内存中存储的是一个指向堆内存的引用(地址),而实际的数据存储在堆内存中。可变性:引用类型的数据是可变的,修改对象的属性会影响所有指向该对象的引用。比较行为:当比较两个引用类型时,比较的是它们的引用(地址),而不是实际值。

常见的引用类型

Object:对象类型(如 { key: 'value' }Array:数组类型(如 [1, 2, 3]Function:函数也是对象(如 function() {}

3. 值类型与引用类型的对比

特性 值类型 引用类型
存储方式 直接存储值 存储对象的引用(地址)
存储位置 栈内存 堆内存
复制行为 复制值 复制引用(指向同一内存地址)
可变性 不可变 可变(可以修改对象的属性)
比较行为 比较实际值 比较内存地址(引用)

4. 注意事项

自动装箱与拆箱:在 JavaScript 中,值类型可以自动转换为对应的对象(装箱),如使用方法时自动将字符串转换为 String 对象,然后使用后自动拆箱回值。类型判断:使用 typeof 可以判断基本类型,但对引用类型(如数组、对象)可能返回 "object",需用 Array.isArray()instanceof 进行进一步判

12.null和undefined的区别

1. 定义

null

表示“无”或“空值”,通常用于指示缺失的对象。是一个赋值类型,可以显式地将变量设置为 null。类型是 object(这是一个语言设计上的历史遗留问题)。

undefined

表示“未定义”,通常用于表示一个变量声明了但未赋值,或者一个对象没有相应的属性。是 JavaScript 的默认值,任何未初始化的变量或函数没有返回值时会自动成为 undefined。类型是 undefined

2. 用法示例

let a; // 声明了但未赋值,默认为 undefined

console.log(a); // 输出: undefined

let b = null; // 明确赋值为 null

console.log(b); // 输出: null

3. 比较

使用 == 比较:

console.log(null == undefined); // 输出: true

在非严格比较中,nullundefined 被视为相等。

使用 === 比较:

console.log(null === undefined); // 输出: false

在严格比较中,二者类型不同,因此不相等。

4. 使用场景

null

用于指示对象的缺失,通常在 API 设计中作为参数或返回值,表示“没有对象”。undefined

通常用于变量未初始化、函数没有返回值或者访问对象不存在的属性时。

13.至少可以说出三种判断JavaScript数据类型的方式,以及他们的优缺点,如何准确的判断数组类型

1. typeof 操作符

优点

简单易用,能够快速判断基本数据类型(如 stringnumberbooleanundefinedfunction)。

缺点

对于对象和数组,typeof 仅返回 "object",无法区分。对于 null 也会返回 "object",这是一个历史遗留问题。

2. instanceof 操作符

优点

可以判断对象是否是某个构造函数的实例,适用于数组、对象、函数等。能够准确判断数组类型。

缺点

对于跨 iframe 或不同执行环境的对象实例,可能会出现问题。只能判断对象的直接原型链,不适用于基本类型。

3. Object.prototype.toString.call()

优点

可以准确判断任何数据类型,包括数组、正则表达式、日期等。结果比较一致,避免了 typeofinstanceof 的局限。

缺点

使用稍显复杂,不够直观。需要调用方法,语法较长。

如何准确判断数组类型

最准确的判断数组类型的方法是使用 Array.isArray()Object.prototype.toString.call()

4. constructor 属性

优点

直接通过对象的构造函数进行判断,语法相对简单。

缺点

对于重写了 constructor 属性的对象,可能会产生误判。只适用于判断对象的构造函数,对于基本数据类型不适用。

5. 使用 instanceof 判断 nullundefined

优点

简单且有效,能快速判断变量是否为对象。

缺点

nullundefined 的判断需要额外处理,因为 null 不会被视为对象。

6. 使用 typeofArray.isArray() 的组合

优点

结合了两种方法的优点,可以在判断是对象的同时确认是否为数组。

缺点

仍然需要注意 null 的情况,因为 typeof null 也是 object

7. JSON.stringify() 方法

优点

可以用来判断 nullundefined,返回的字符串比较简单。

缺点

这种方法不够直接,也可能导致误解,因为它只适用于特定情况。

8. 自定义类型判断函数

可以创建一个自定义函数,综合以上方法来判断数据类型:

function getType(variable) {

if (variable === null) return 'null';

if (Array.isArray(variable)) return 'array';

return typeof variable; // 其他类型

}

console.log(getType([1, 2, 3])); // 输出: "array"

console.log(getType(null)); // 输出: "null"

console.log(getType(42)); // 输出: "number"

14.可能发生隐式类型转换的场景以及转换原则,应如何避免或巧妙应用

1. 运算符的使用

算术运算:当涉及不同类型的值时,JavaScript 会尝试将其转换为数字。

console.log('5' - 2); // 输出: 3(字符串 '5' 转换为数字)

console.log('5' + 2); // 输出: '52'(数字 2 转换为字符串)

比较运算:在使用 == 时,JavaScript 会进行类型转换。

console.log(0 == false); // 输出: true

console.log('' == false); // 输出: true

2. 条件判断

在条件语句中,所有值都会被转换为布尔值:

if ('') { // 空字符串被视为 false

console.log('This will not run');

}

3. 函数参数

如果函数参数期望特定类型,而传入了不同类型,JavaScript 会进行转换:

function add(a, b) {

return a + b;

}

console.log(add('5', 2)); // 输出: '52'(隐式转换为字符串)

隐式转换原则

优先转换为数字:在算术运算中,字符串会转换为数字。相等比较时:使用 == 进行比较时,会尝试转换为相同类型。

如何避免或巧妙应用

使用严格相等(===:始终使用 ===!== 进行比较,以避免意外的类型转换。

明确类型转换:在需要时使用显式转换,确保数据类型符合预期。

使用 parseIntparseFloat:对于字符串到数字的转换,使用这些函数来确保转换方式。

避免空值的使用:尽量避免使用 nullundefined 作为比较或运算的参与者,确保变量有明确的值。

清晰的函数参数:在函数中使用参数类型检查,确保传入的值符合预期类型。

15.出现小数精度丢失的原因,JavaScript可以存储的最大数字、最大安全数字,JavaScript处理大数字的方法、避免精度丢失的方法

console.log(0.1 + 0.2); // 输出: 0.30000000000000004

这是因为 0.1 和 0.2 在二进制中不能精确表示,导致计算结果出现误差。

JavaScript 可以存储的最大数字

最大数字Number.MAX_VALUE,约为 1.7976931348623157e+308最小数字Number.MIN_VALUE,约为 5e-324(接近于零,但不等于零)。

最大安全数字

最大安全整数Number.MAX_SAFE_INTEGER,值为 9007199254740991(即 253−12^{53} - 1253−1)。最小安全整数Number.MIN_SAFE_INTEGER,值为 -9007199254740991

在这个范围内的整数可以准确地表示,而超过这个范围的整数可能会出现精度丢失。

JavaScript 处理大数字的方法

使用 BigInt:这是 ES2020 引入的一种新数据类型,可以表示任意大小的整数。

使用第三方库:可以使用如 decimal.jsbignumber.js 等库来处理高精度的小数和大数字。

避免精度丢失的方法

整数运算:尽量将小数转换为整数进行计算。例如,将所有金额以分为单位处理

使用 toFixed():虽然不能完全解决精度丢失,但可以控制小数点后位数

使用 Math.round():在进行浮点数运算后进行四舍五入

使用 BigDecimal 类型:如果你的应用需要极高的精度,考虑使用 BigDecimal 类型(如通过库实现)。

16.理解原型设计模式以及JavaScript中的原型规则

关键点

原型对象:对象可以从其他对象继承属性和方法。对象的共享:通过原型共享方法和属性,节省内存。

JavaScript 中的原型规则

JavaScript 使用原型链来实现继承和对象的共享。这些规则可以总结为以下几点:

每个对象都有一个原型

当你创建一个对象时,它会自动继承自 Object.prototype,或者其构造函数的 prototype 属性。可以通过 Object.getPrototypeOf(obj)obj.__proto__ 获取对象的原型。

原型链

当访问对象的属性或方法时,JavaScript 会首先检查对象自身是否有该属性,如果没有,则沿着原型链向上查找。如果原型链上的对象也没有该属性,最终会查找到 null

构造函数与原型

通过构造函数创建对象时,可以在构造函数的 prototype 属性上定义共享的方法和属性。

function Person(name) {

this.name = name;

}

Person.prototype.greet = function() {

console.log(`Hello, my name is ${this.name}`);

};

const alice = new Person('Alice');

alice.greet(); // 输出: Hello, my name is Alice

        4. 原型属性的覆盖

如果在对象中定义了与原型中同名的属性,优先访问对象自身的属性,原型中的属性被遮蔽。

5.instanceof 运算符

用于检查对象是否是某个构造函数的实例,底层是通过原型链实现的

17.理解JavaScript的作用域和作用域链

1. 全局作用域

在 JavaScript 中,任何在全局上下文中声明的变量或函数都属于全局作用域。这些变量和函数可以在代码的任何位置访问。在浏览器中,全局作用域是 window 对象。

2. 局部作用域

局部作用域是指在函数内部声明的变量或函数。这些变量只能在该函数内部访问。每个函数都有自己的作用域,局部作用域会优先于全局作用域。

作用域链

作用域链是一个机制,用于确定在特定上下文中变量的可访问性。每当代码执行时,JavaScript 引擎会创建一个执行上下文,并维护一个作用域链。

1. 链的结构

当代码在某个执行上下文中运行时,作用域链会从当前作用域向上查找,直到找到变量或到达全局作用域。这意味着,如果在当前作用域中找不到某个变量,JavaScript 会在其父作用域中继续查找。

let outerVar = 'I am outside';

function outerFunction() {

let innerVar = 'I am inside';

function innerFunction() {

console.log(innerVar); // 可以访问

console.log(outerVar); // 可以访问

}

innerFunction();

}

outerFunction(); // 输出: I am inside \n I am outside

2. 闭包

当内部函数访问外部函数的变量时,就形成了闭包。闭包可以使得外部函数的变量在内部函数中保持状态,即使外部函数已经返回。

function makeCounter() {

let count = 0; // 局部变量

return function() {

count++; // 访问外部变量

return count;

};

}

const counter = makeCounter();

console.log(counter()); // 输出: 1

console.log(counter()); // 输出: 2

18.instanceof typeof 的底层实现原理,手动实现一个instanceof typeof

instanceoftypeof 的底层实现原理

1. typeof

typeof 是一个操作符,用于检查变量的类型。它返回一个表示类型的字符串。

实现原理

typeof 会根据变量的内部类型来返回相应的字符串。例如,原始类型(如 stringnumberbooleanundefined)和对象(如数组、函数、对象等)都有特定的表示方式。

typeof 'hello'; // "string"

typeof 42; // "number"

typeof true; // "boolean"

typeof undefined; // "undefined"

typeof {}; // "object"

typeof []; // "object"

typeof function() {}; // "function"

2. instanceof

instanceof 是一个操作符,用于检查对象是否是某个构造函数的实例。它通过查找对象的原型链来实现。

实现原理

instanceof 会检查对象的 __proto__ 属性是否指向构造函数的 prototype 属性。如果找到了匹配,则返回 true,否则返回 false

function Person() {}

const alice = new Person();

console.log(alice instanceof Person); // true

console.log(alice instanceof Object); // true

手动实现 typeofinstanceof

1. 手动实现 typeof

可以通过 Object.prototype.toString.call() 来模拟 typeof 的部分功能:

function customTypeof(value) {

if (value === null) return 'null'; // 特殊情况

return typeof value === 'object' ?

Object.prototype.toString.call(value).slice(8, -1).toLowerCase() :

typeof value;

}

console.log(customTypeof('hello')); // "string"

console.log(customTypeof(42)); // "number"

console.log(customTypeof(null)); // "null"

console.log(customTypeof([])); // "array"

console.log(customTypeof({})); // "object"

console.log(customTypeof(function() {})); // "function"

2. 手动实现 instanceof

可以通过一个简单的函数来实现 instanceof 的功能:

function customInstanceof(instance, constructor) {

if (typeof instance !== 'object' || instance === null) return false;

let prototype = constructor.prototype;

// 检查原型链

while (instance) {

if (instance === prototype) return true;

instance = Object.getPrototypeOf(instance);

}

return false;

}

function Person() {}

const alice = new Person();

console.log(customInstanceof(alice, Person)); // true

console.log(customInstanceof(alice, Object)); // true

console.log(customInstanceof({}, Person)); // false

19.实现继承的几种方式以及他们的优缺点

1. 原型链继承

通过将子类的原型指向父类的实例,实现继承。

function Parent() {

this.name = 'Parent';

}

Parent.prototype.sayHello = function() {

console.log(`Hello from ${this.name}`);

};

function Child() {

this.name = 'Child';

}

Child.prototype = new Parent();

const child = new Child();

child.sayHello(); // 输出: Hello from Parent

2. 构造函数继承

在子类构造函数中调用父类构造函数,使用 callapply

function Parent(name) {

this.name = name || 'Parent';

}

function Child(name) {

Parent.call(this, name); // 传递参数

}

const child = new Child('Child');

console.log(child.name); // 输出: Child

3. 组合继承

结合原型链继承和构造函数继承的优点。

function Parent(name) {

this.name = name || 'Parent';

}

Parent.prototype.sayHello = function() {

console.log(`Hello from ${this.name}`);

};

function Child(name) {

Parent.call(this, name); // 传递参数

}

Child.prototype = Object.create(Parent.prototype); // 继承原型

Child.prototype.constructor = Child;

const child = new Child('Child');

child.sayHello(); // 输出: Hello from Child

4.寄生式继承

在构造函数内部创建一个新对象,并将父类的方法添加到这个新对象上。

function createChild(parent) {

const child = Object.create(parent);

child.sayHello = function() {

console.log(`Hello from ${this.name}`);

};

return child;

}

const parent = { name: 'Parent' };

const child = createChild(parent);

child.name = 'Child';

child.sayHello(); // 输出: Hello from Child

5. ES6 的 class 继承

使用 ES6 的 class 语法来实现继承。

class Parent {

constructor(name) {

this.name = name || 'Parent';

}

sayHello() {

console.log(`Hello from ${this.name}`);

}

}

class Child extends Parent {

constructor(name) {

super(name); // 调用父类构造函数

}

}

const child = new Child('Child');

child.sayHello(); // 输出: Hello from Child

20.在 JavaScript 中,数组的某些方法会改变原数组(即就地修改),而其他方法则不会改变原数组,而是返回一个新数组。

改变原数组的方法

push()

向数组末尾添加一个或多个元素

const arr = [1, 2, 3];

arr.push(4); // arr 变为 [1, 2, 3, 4]

pop()

从数组末尾删除一个元素,并返回该元素

const arr = [1, 2, 3];

arr.pop(); // arr 变为 [1, 2]

shift()

从数组开头删除一个元素,并返回该元素

const arr = [1, 2, 3];

arr.shift(); // arr 变为 [2, 3]

unshift()

向数组开头添加一个或多个元素

const arr = [1, 2, 3];

arr.unshift(0); // arr 变为 [0, 1, 2, 3]

splice()

从数组中添加或删除元素

const arr = [1, 2, 3];

arr.splice(1, 1); // arr 变为 [1, 3],删除索引1的元素

sort()

对数组进行排序

const arr = [3, 1, 2];

arr.sort(); // arr 变为 [1, 2, 3]

reverse()

反转数组的元素顺序

const arr = [1, 2, 3];

arr.reverse(); // arr 变为 [3, 2, 1]

不改变原数组的方法

map()

创建一个新数组,包含对原数组每个元素调用函数后的结果

const arr = [1, 2, 3];

const newArr = arr.map(x => x * 2); // newArr 为 [2, 4, 6]

filter()

创建一个新数组,包含所有通过测试的元素

const arr = [1, 2, 3, 4];

const evenArr = arr.filter(x => x % 2 === 0); // evenArr 为 [2, 4]

reduce()

对数组中的每个元素执行一个 reducer 函数,并返回单个值

const arr = [1, 2, 3, 4];

const sum = arr.reduce((acc, x) => acc + x, 0); // sum 为 10

slice()

返回数组的一个片段,生成一个新数组,原数组不变

const arr = [1, 2, 3, 4];

const newArr = arr.slice(1, 3); // newArr 为 [2, 3]

concat()

合并两个或多个数组,返回新数组,原数组不变

const arr1 = [1, 2];

const arr2 = [3, 4];

const newArr = arr1.concat(arr2); // newArr 为 [1, 2, 3, 4]

find()

返回数组中第一个满足条件的元素,不改变原数组

const arr = [1, 2, 3, 4];

const found = arr.find(x => x > 2); // found 为 3

some()

测试数组中是否至少有一个元素通过测试,不改变原数组

const arr = [1, 2, 3];

const hasEven = arr.some(x => x % 2 === 0); // hasEven 为 true

every()

测试数组中的所有元素是否都通过测试,不改变原数组

const arr = [2, 4, 6];

const allEven = arr.every(x => x % 2 === 0); // allEven 为 true

总结

改变原数组的方法push()pop()shift()unshift()splice()sort()reverse()不改变原数组的方法map()filter()reduce()slice()concat()find()some()every()



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。