前端杂学录(一)
真的不想学习啦 2024-10-02 13:03:01 阅读 69
记录看到的前端知识
1.<code>const、let
和 var
是 JavaScript 中用于声明变量的三种方式,它们之间有一些重要的区别和用法。
1. var
作用域:var
声明的变量是函数作用域或全局作用域。在块级作用域(如 if
、for
等)中声明的 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:布尔值,只有两个取值:true
和 false
。Number:数字类型,包括整数和浮点数。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):数组实际上也是一种对象,但它的键是数字索引,底层可能会采用不同的优化方式(如稀疏数组处理)。Map
和 Set
:这两种数据结构也是基于哈希表实现的,但它们比普通对象支持更多的数据类型作为键。
6.JavaScript 中的 Map
和 Set
JavaScript 提供了 Map
和 Set
,它们是高级的数据结构,内部使用哈希表来实现,允许高效地存储、查找和删除数据。
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
, array
some()
:检查数组中是否有任意元素满足条件。every()
:检查数组中是否所有元素都满足条件。
find()
/ findIndex()
参数顺序: value
, index
, array
find()
:返回第一个满足条件的元素。findIndex()
:返回第一个满足条件的元素的索引。
2. Object
方法
Object.entries()
返回:[key, value]
数组的迭代器。将对象的键值对转换为数组,每个元素是 [key, value]
。
Object.keys()
返回:对象的键组成的数组
Object.values()
返回:对象的值组成的数组。
3. Map
和 Set
方法
map.forEach()
参数顺序: value
, key
, map
遍历 Map
中的每个键值对。
set.forEach()
参数顺序: value
, valueAgain
, set
遍历 Set
中的每个元素,value
和 valueAgain
是相同的,因为 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
(函数)其他如 Date
、RegExp
等复杂数据类型
这些数据类型的值在堆内存中存储。变量本身并不直接存储值,而是存储一个引用,这个引用指向存储在堆内存中的数据。
特点:
引用类型的数据存储在堆中,变量保存的是对堆内存的引用。访问对象、数组等引用类型时,实际上是通过栈中的引用去查找堆中的数据。当多个变量引用同一个对象时,它们指向的都是堆内存中的同一块地址。
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 | 无 |
说明:
Null
和 Undefined
没有对应的内置对象,因此它们不能通过装箱操作转化为对象。其他基本类型都有内置对象,允许我们调用某些方法,如 String
的 length
,Number
的 toFixed()
等。
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
:数字类型(如 1
、3.14
)String
:字符串类型(如 "hello"
)Boolean
:布尔类型(true
或 false
)Null
:表示空值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
在非严格比较中,null
和 undefined
被视为相等。
使用 ===
比较:
console.log(null === undefined); // 输出: false
在严格比较中,二者类型不同,因此不相等。
4. 使用场景
null
:
用于指示对象的缺失,通常在 API 设计中作为参数或返回值,表示“没有对象”。undefined
:
通常用于变量未初始化、函数没有返回值或者访问对象不存在的属性时。
13.至少可以说出三种判断JavaScript数据类型的方式,以及他们的优缺点,如何准确的判断数组类型
1. typeof
操作符
优点
简单易用,能够快速判断基本数据类型(如 string
、number
、boolean
、undefined
和 function
)。
缺点
对于对象和数组,typeof
仅返回 "object"
,无法区分。对于 null
也会返回 "object"
,这是一个历史遗留问题。
2. instanceof
操作符
优点
可以判断对象是否是某个构造函数的实例,适用于数组、对象、函数等。能够准确判断数组类型。
缺点
对于跨 iframe 或不同执行环境的对象实例,可能会出现问题。只能判断对象的直接原型链,不适用于基本类型。
3. Object.prototype.toString.call()
优点
可以准确判断任何数据类型,包括数组、正则表达式、日期等。结果比较一致,避免了 typeof
和 instanceof
的局限。
缺点
使用稍显复杂,不够直观。需要调用方法,语法较长。
如何准确判断数组类型
最准确的判断数组类型的方法是使用 Array.isArray()
或 Object.prototype.toString.call()
。
4. constructor
属性
优点
直接通过对象的构造函数进行判断,语法相对简单。
缺点
对于重写了 constructor
属性的对象,可能会产生误判。只适用于判断对象的构造函数,对于基本数据类型不适用。
5. 使用 instanceof
判断 null
和 undefined
优点
简单且有效,能快速判断变量是否为对象。
缺点
null
和 undefined
的判断需要额外处理,因为 null
不会被视为对象。
6. 使用 typeof
和 Array.isArray()
的组合
优点
结合了两种方法的优点,可以在判断是对象的同时确认是否为数组。
缺点
仍然需要注意 null
的情况,因为 typeof null
也是 object
。
7. JSON.stringify()
方法
优点
可以用来判断 null
和 undefined
,返回的字符串比较简单。
缺点
这种方法不够直接,也可能导致误解,因为它只适用于特定情况。
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'(隐式转换为字符串)
隐式转换原则
优先转换为数字:在算术运算中,字符串会转换为数字。相等比较时:使用 ==
进行比较时,会尝试转换为相同类型。
如何避免或巧妙应用
使用严格相等(===
):始终使用 ===
和 !==
进行比较,以避免意外的类型转换。
明确类型转换:在需要时使用显式转换,确保数据类型符合预期。
使用 parseInt
和 parseFloat
:对于字符串到数字的转换,使用这些函数来确保转换方式。
避免空值的使用:尽量避免使用 null
和 undefined
作为比较或运算的参与者,确保变量有明确的值。
清晰的函数参数:在函数中使用参数类型检查,确保传入的值符合预期类型。
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.js
或 bignumber.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
instanceof
和 typeof
的底层实现原理
1. typeof
typeof
是一个操作符,用于检查变量的类型。它返回一个表示类型的字符串。
实现原理:
typeof
会根据变量的内部类型来返回相应的字符串。例如,原始类型(如 string
、number
、boolean
、undefined
)和对象(如数组、函数、对象等)都有特定的表示方式。
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
手动实现 typeof
和 instanceof
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. 构造函数继承
在子类构造函数中调用父类构造函数,使用 call
或 apply
。
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()
。
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。