一. var
、let
、const
区别
1.作用域
var
属于函数级作用域 ,在if/for
等块中声明的变量会泄漏到外部函数作用域。let/const
是块级作用域 ({}
内生效),能精准控制变量生命周期。
经典for循环定时器问题:
var : 是因为在退出循环时,迭代变量保存的是导致循环退出的值:5,在之后执行超时逻辑时,所有i都是同一个变量,因而输出的都是同一个最终值
let : 为每次循环创建独立作用域
// var 的问题
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i)) } // 输出 3,3,3
// let 的解决方案
for (let j = 0; j < 3; j++) { setTimeout(() => console.log(j)) } // 输出 0,1,2
2.提升
var : 会被提升到函数或全局的顶部,但只有变量声明部分会被提升,赋值部分不会
let 和 const: 会被提升到顶部,但在实际声明之前,它们处于一个"暂时性死区",这意味着在声明之前访问它们会抛出错误
3. 重新赋值
var 和 let 可以重新赋值
const : 声明的变量必须初始化,而且不能重新赋值,一旦赋值,值是常量
注意:const 只保证变量绑定的引用不变,对于对象或数组,引用地址不可变,但其内部内容是可修改的
4.全局对象属性
var 在全局作用域声明时,会在全局对象 window或global上创建属性
let 和const : 在全局作用域声明时,不会在全局对象上创建属性
二. 数据类型和typeof的陷阱
1.typeof 返回object 但实际上是null
- 陷阱: typeof null 会返回"object",这可能会让人感到困惑,因为null其实是一个空值,并不是一个对象
原因:这是一个历史遗留的bug,早期js中,null的类型被设计为对象,虽然这一点已经被修复,但为了兼容性,js保持了这个行为
解决办法: 可以使用=== 来判断null 例如
if(value===null){
//处理为null的情况
}
2.数组的typeof 返回object
-
陷阱: typeof 对数组的返回值是"object",这可能让人误以为它是一个普通对象,而不是一个数组
原因: 在js中,数组是对象的一种特殊类型,因此, typeof 对数组返回的仍然是object,这就造成了便是数组时的困惑
解决办法: 使用Array.isArray()方法来判断一个值是否为数组,这是专门用来检查数组的标准方法
3. typeof 对函数返回值是"function"
陷阱: typeof 对函数的返回值是function
4.NaN 是一个数字,但不等于任何值(包括它自己)
NaN 是一个特殊的数字,常见于运算错误或无效转换,且它与任何值都不相等,包括它自己
如果你检查一个值是否NaN,不能直接用 === 来比较,因为NaN=== NaN 是false,可以使用Number.isNaN()来判断:
console.log(Number.isNaN(NaN)); // true
console.log(NaN === NaN); // false
三. js中的值类型和引用类型
在js中,数据类型分为两类: 值类型和引用类型,它们的主要区别在于存储和传递数据的方式
1.值类型
值类型也叫原始类型,它们存储的实际的数据值,当你将一个值类型的数据赋值给另一个变量时,它们之间是完全独立的.改变一个变量的值不会影响另一个
值类型的特点:
- 直接存储数据值
- 赋值时,会创建数据的副本(传值)
- 不可变,即值类型的数据一旦创建就无法改变(但可以重新赋值)
- 比较时,直接比较值
值类型的数据包括:
- undefined: 只有一个值 undefined
- null: 代表控制
- boolean: true或false
- number: 包括整数和浮动数值(NaN和Infinity也属于number类型)
- string: 字符串类型
- symbol: ES6引入,表示唯一的标识符
- bigint: 用于标识非常大的整数(ES11引入)
2. 引用类型(Reference Types)
引用类型的数据存储的是内存地址(即引用),而不是数据本身,每当我们将一个引用类型的值赋值给另一个变量时,两个变量都会指向同一块内存地址,因此他们是共享同一个数据对象的
引用类型的特点:
- 存储的是指向实际数据的引用(内存地址)
- 赋值时,不会创建副本,而是复制引用(传址)
- 可以被修改,即引用类型的数据是可变的.
- 比较时,比较的是引用的内存地址.
引用类型的数据包括:
- object: 包括普通对象,数组,函数等
- array: 数组是对象的特殊类型
- function: 函数也是对象类型
- date,regexp等也是对象类型
3.关键考点:
1. 函数参数传递
- js永远是按值传递
- 值类型: 传递值的副本
- 引用类型: 传递引用的副本
function modify(obj){
//修改属性,会影响外部
obj.name='Modified';
/**分析*//:
函数内的obj和外部的myObj指向同一个地址,通过该地址修改堆内存中的对象,会影响外部
//fragment
//重新赋值,不会影响外部
obj={name: 'New Object'}
分析:
- 将函数内的obj变量指向了一个新的内存地址.
- 这与myObj断开了链接,后续修改互不相干
}
// fragment
let myObj={ name: 'Initial'};
modify(myobj);
console.log(myObj.name)? //Modified
2.const 的不变性
- const保证的是变量存储的地址不可变
- 不保证地址指向的对象内容不可变
const user={name:'john'};
user.name='Jane': //合法: 修改对象内部属性
//非法: 尝试修改user变量的地址
user={name:'Doe'}; //TypeError
3.相等性判断(===)
- 值类型: 比较值是否相等
- 引用类型: 比较引用/地址 是否相等
console.log(100===100); //true
//引用类型比较
console.log({ }==={ }); //false(两个独立的地址)
fragement
let objA={ id:1}
let objB=objA;
console.log(objA === objB); //true 指向同一个地址
四. 深拷贝与浅拷贝的本质
1.浅拷贝
浅拷贝是指只复制对象的第一层数据,对于嵌套的对象或数组,它只会复制引用,而不会复制嵌套对象的内容,因此,浅拷贝产生的对象和原始对象中的嵌套部分(如数组,对象等)是共享的,修改其中一个嵌套对象会影响另一个
本质:
- 浅拷贝创建一个新的对象或数组,复制的是对象的第一层属性
- 对象的嵌套结构(引用类型),浅拷贝后仍然是引用类型,嵌套对象并没有复制,而是直接引用了原始对象中的值
浅拷贝的方式:
- object.assign() : 复制对象的属性
- 扩展运算符(...): 用于浅拷贝对象
- 数组的slice( )或concat( ): 复制数组
手写
function shallowCopy(obj){
//判断是否为对象或数组
if(typeof obj !=='object' || obj===null){
return obj; 如果不是对象或数组,直接返回原始值
}
//创建一个新对象或数组
let copy=Array.isArray(obj)? [ ]: {};
for(let key in obj){
//检查一个属性是否是对象的直接属性,而不是继承自原型链
if(obj.hasOwnProperty(key)){
copy[key]=obj[key]
}
}
return copy;
}
2.深拷贝(Deep Copy)
深拷贝是指完全复制对象及其所有嵌套的对象,即使对象内部还有其他对象或数组,深拷贝会递归地复制每一个嵌套的部分,深拷贝创建的是完全独立的新对象,新对象中每个层次的数据都被复制了,不会共享任何内存地址
本质:
深拷贝会递归地复制对象的每一层
- 嵌套对象会被复制成全新的对象,因此它们之间没有任何关联
深拷贝的方式:
-
使用JSON.parse( )和 JSON.stringify( ): 适用于对象不含函数,undefined,symbol 等特殊值的情况
缺陷: 忽略undefined,忽略Symbol,忽略Function,Date对象会变成字符串,无法处理循环引用(会报错)
-
手动递归复制: 需要针对每个属性进行处理(更复杂,但适应性强)
-
第三方库: 如Lodash 的 cloneDeep( )方法
3.手写深拷贝函数
function deepCopy(obj){
//判断是否为对象或数组
if(typeof obj !== 'object' || obj===null){
return obj //如果不是对象或数组,直接返回原值
}
//创建一个新对象或数组
let copy = Array.isArray(obj) ? [ ] : { };
for(let key in obj) {
//递归调用deepCopy 对象或数组中的每一层
copy[key] = deepCopy(obj[key]);
}
return copy;
}
五. 数组的常用方法
1.数组的添加和删除
- push( ) 向数组的末尾添加一个或多个元素,返回新数组的长度
- pop( ): 从数组的末尾删除一个元素,并返回删除的元素
- shift( ): 从数组的开头删除一个元素,并返回删除的元素
- unshift( ): 向数组的开头添加一个或多个元素,返回新数组的长度
- splice( ): 从数组中添加或删除元素,可以删除指定位置的元素,或者在指定位置插入元素,它会改变原始数组
2.数组查找元素
- indexOf( ): 返回指定元素在数组中的第一个索引,若没有找到返回-1
- includes( ): 判断数组中是否包含指定元素,返回true或fasle
- find( ): 返回数组中第一个满足条件的元素,如果没有找到则返回undefined
- findIndex( ): 返回数组中第一个满足条件的元素的索引,如果没有找到则返回-1
3.数组遍历
- forEach( ): 对数组的每个元素执行指定的函数,没有返回值
- map( ): 返回一个新数组,数组的元素是通过调用指定函数处理原数组中的每个元素得到的
var arr = [1, 2, 3];
var doubled = arr.map(function(x) {
return x * 2;
});
console.log(doubled); // [2, 4, 6]- filter( ): 返回一个新数组,包含所有符合条件的元素
var arr = [1, 2, 3, 4, 5];
var even = arr.filter(function(x) {
return x % 2 === 0;
}); // [2, 4]
console.log(even);- reduce( ): 对数组中的每个元素执行指定的函数,并返回最终的计算结果.可以用来累加,求和等.
var arr = [1, 2, 3, 4];
var sum = arr.reduce(function(acc, current) {
return acc + current;
}, 0); // 10
console.log(sum);- some( ): 判断数组中是否至少有一个元素满足条件,返回true,或false
var arr = [1, 2, 3, 4];
var hasEven = arr.some(function(x) {
return x % 2 === 0;
}); // true
console.log(hasEven);- every(): 判断数组中每个元素是否都满足条件,返回true或false.
var arr = [2, 4, 6, 8];
var allEven = arr.every(function(x) {
return x % 2 === 0;
}); // true
console.log(allEven);
4.数组排序和反转
- sort( ): 对数组进行排序(默认按字典顺序排序).可以传入排序函数自定义排序规则
- reverse( ): 反转数组的元素
5.数组合并和切割
- concat( ): 合并两个或多个数组,返回一个新数组
var arr1 = [1, 2];
var arr2 = [3, 4];
var merged = arr1.concat(arr2); // [1, 2, 3, 4]
console.log(merged); - slice( ): 返回数组的一个浅拷贝,包含从start到end(不包括end之间的所有元素)
var arr = [1, 2, 3, 4];
var sliced = arr.slice(1, 3); // [2, 3]
console.log(sliced);
6.数组的其他常用方法
-
join( ): 将数组的所有元素连接成一个字符串,默认用逗号分割
var arr = ['apple', 'banana', 'cherry'];
var str = arr.join(', '); // "apple, banana, cherry"
console.log(str); -
flat( ): 将多维数组"拉平"成以维数组
var arr = [1, [2, 3], [4, [5, 6]]];
var flattened = arr.flat(2); // [1, 2, 3, 4, 5, 6]
console.log(flattened); -
flatMap( ): 先对每一个元素执行map( )操作,再对结果执行flat()
var arr = [1, 2, 3];
var result = arr.flatMap(function(x) {
return [x, x * 2];
}); // [1, 2, 2, 4, 3, 6]
console.log(result);
六. 对象的遍历方式对比
1.for...in 循环
for...in 是遍历对象的最常用的方法之一,它遍历对象的所有可枚举属性,包括从原型链继承的属性
特性:
- 会遍历对象的所有可枚举属性(包括原型链上的属性)
- 可以用来遍历对象的属性名
var obj = { a: 1, b: 2, c: 3 };
for (var key in obj) {
if (obj.hasOwnProperty(key)) { // 过滤掉继承的属性
console.log(key + ': ' + obj[key]);
}
}
可能会遍历原型链上的属性,需要使用hasOwnProperty( )来过滤继承的属性
2.Object.keys( )
返回一个由对象自身的所有可枚举属性的键(属性名)组成的数组,遍历这个数组可以遍历对象的所有属性.
特性:
- 只会返回对象自身的可枚举属性(不包括继承的属性)
- 返回的是一个数组,可以使用数组方法(如forEach( ),map( )等)进行遍历.
var obj = { a: 1, b: 2, c: 3 };
var keys = Object.keys(obj);
keys.forEach(function(key) {
console.log(key + ': ' + obj[key]);
});
返回的是一个数组,不能直接访问值,需要额外的循环或方法
3.object.values( )
返回一个包含对象所有可枚举属性值的数组,适用于直接操作值
特性:
- 只会返回对象自身的可枚举属性的值
- 返回的值是数组,可以使用数组方法进行遍历
var obj = { a: 1, b: 2, c: 3 };
var values = Object.values(obj);
values.forEach(function(value) {
console.log(value);
});
4.Object.entries( )
返回一个包含对象自身所有可枚举属性[key,value]的数组,每个元素是一个数组,第一个元素是键,第二个是值.
特性:
- 返回[key,value]的二维数组,可以通过数组的操作方法进行处理
- 只返回对象自身的可枚举属性
var obj = { a: 1, b: 2, c: 3 };
var entries = Object.entries(obj);
entries.forEach(function(entry) {
var key = entry[0];
var value = entry[1];
console.log(key + ': ' + value);
});
5.Object.getOwnPropertyNames( )
返回对象自身的所有字符串属性名(含不可枚举属性)
6.Object.getOwnPropertySymbols( ) 返回对象自身的所有Symbol属性名(含不可枚举属性)
7.Reflect.ownKeys( )
特点
- 返回对象所有自身属性(包括不可枚举属性和Symbol属性)
- 可替代Object。getOwnPropertyNames()+ Object.getOwnPropertySymbols()
8.性能与陷阱
1.性能排序(从高到低)
Object.keys()
≈ for...in
(现代引擎) > Object.entries()
> Reflect.ownKeys()
。
- 原因: Object.keys( )等不遍历原型链,且现代引擎优化了for...in; Reflect.ownKeys( )需扫描全属性
2.常见陷阱:
- for...in 可能意外遍历原型属性-> 始终用hasOwnProperty过滤
- Object.entries( ) 对数字键自动转化为字符串(如{ 1:'a'}-> ["1",'a'])
- 大对象频繁调用Object.keys( )可能触发内存压力(需创建新数组 )
七. 隐式类型转换有哪些坑?
隐士类型转换是什么?
定义: 运算或比较时,js引擎自动,静默地转换操作数类型
1.宽松相等(==)的转换陷阱
1.数字与字符串相比
1=="1" //true 字符串转数字
字符串被隐式转为数字,非数字字符串(如'abc')转为NaN,导致false
可以使用 === 避免转换
2.布尔值与其他类型比较
false == '0' //true false->0 '0'->0
true == '1' //true true->1 '1'=>1
true == 2 //false
3.null 与 undefined 的特殊规则
null == undefined //true 规范规定
null == 0 //false (null 不转数字)
undefined == ' ' //false
null / undefined 仅互等,与其他值比较均返回false
2.算数运算中的类型陷阱
1.加法运算的歧义
5 + '10' // "510" (数字转字符串拼接)
5 + null // 5(null->0)
\] + { } //"\[object object\]" (空数组-\> 对象转字符串)\[2,3\](@ref)
规则: + 运算优先字符串拼接,其他运算符(-,*)强制转数字
2.对象参与的运算
let obj ={ valueOf:( )=> 10};
5+obj //15 (调用valueOf( )) [3,4] (@ref)
- 转换顺序: 对象先调用valueOf( ), 若返回非原始值再调toString( )
3.布尔转换的"假值"陷阱
js中只有7个假值:
false,0," ",null,undefined,NaN,document.all(历史遗留)
易错场景:
if([ ]){ console.log('执行')} //空数组为真值 true
Boolean([ ]) true
Boolean(0) //false
Boolean('0') //true 直接布尔转换-> 非空字符串即为true
Boolean( ' ') //false
Boolean(0) //false
4.NaN 不等于任何值,包括它本身
NaN==NaN false
5.对象/数组转原始值
1\] =='1' //true \[1\].toString( )为'1',所以\[1\]==='1' 是true \[0\] == 0 //true \[null\] == 0 //true { } == { } //false
规则:数组先转字符串join(), 在转数字
6.toStirng( )和valueOf( ) 在隐式类型转换中的作用
什么情况下会触发这两个方法?
当对象(包括数组,普通对象,函数)与原始值参与运算或比较时,js会试图把对象转成原始值(string,number,boolean),这时候会调用:
- Symbol.toPrimitive(如果有的话,优先调用)
- 否则依次调用: valueof( ) -> toString( ) 依赖转换期望类型
1.对象转原始值的内部规则
内部会指定一种"期望类型" 去尝试转换:number 或string
转换顺序(默认):
期望类型 : number 调用顺序 valueOf( ) -> toString( )
string 调用顺序: toString( ) -> valueof( )
const obj={
valueOf( ){
return 100;
},
toSstring( ) {
return "hello"
}
};
console.log(obj+1); //101 -> 先走valueOf()
console.log(String(obj)); //hello->先走 toStirng( )
- Array的表现(toString) 非常重要
1,2,3\].toString( ); //1 2 3 \[null\].toString( ); // ' ' \[ \[ \]\].toStrinng( ); // ' ' \[\[1\]\].toStrinng( ); // '1'
数组的valueOf( ) 返回数组的本身(对象)
所以一般会调用toString( ) 来转换为字符串
八. == 和 === 的区别?
1.== (宽松相等,类型转换比较)
作用: == 会先进行隐式类型转换,然后再比较
行为:它会尝试将左右两边的值转换为相同类型后再进行比较,如果转换成功且值相等,则返回true,否则返回false
0 == false //true 0-> 转换为false
'0' == 0 //true ('0'转换为0)
null == undefined //true (null和undefined 相等)
\]== ' ' //true 空数组转为空字符串
2. === (严格相等,不进行类型转换)
===进行严格相等比较,不仅比较值是否相等,还会比较诶行
只有左右两边的值类型相同且值相等时,===才会返回true
0===false //false 类型不同
'0' === 0 //false
null == undefined //false
\] === ' ' //false
九.执行上下文和作用域链是什么?
1. 执行上下文
执行上下文时js执行代码时的环境,每当js引擎开始执行一段代码时,它会创建一个新的执行上下文,这个上下文包含了代码执行的各种信息,例如变量,函数,当前执行位置等
类型 :
- 全局执行上下文:整个程序的最外层上下文,执行代码时默认存在一个全局上下文,在浏览器中,它与window对象相关联
- 函数执行上下文: 每当函数被调用时,都会创建一个新的执行上下文,每个函数都有自己的执行上下文
创建阶段:
- 变量对象(VO):在创建阶段,会为每个执行上下文创建一个变量对象,存储函数参数,声明的变量和声明的函数,对于函数上下文,还会存储arguments对象
2.作用域链: 决定变量查找的顺序,作用域链包含当前执行上下文的作用域以及它的外部作用域
3.this绑定: 在执行上下文中,this会根据当前的上下文来绑定
执行阶段:
代码开始执行时,变量和函数的赋值会发生,执行具体的操作
执行上下文栈:
JS是单线程的,它将执行上下文保存在栈中,每次进入一个函数时,新的上下文会被压入栈中,执行完毕后会弹出栈
2.作用域链(Scope Chain)
作用域链是由多个作用域组成的链条,每次执行代码时,JS会按照作用域链的顺序查找变量.最勇于链从当前执行上下文的作用域开始,逐层向外层作用域查找,直到全局作用域
作用域链的结构:
1.当前执行上下文的作用域: 首先查找当前执行上下文中的变量
2. 外层作用域: 如果当前上下文找不到,JS会在外层作用域查找,直到直到变量为止
3.全局作用域: 最后,会查找全局作用域中的变量 .
3.为什么需要理解它们?
- 理解变量的查找机制:作用域链理解了为什么以及如何在一个函数内部访问到外部函数甚至全局作用域中的变量,这是理解闭包的基础
2.解释var 的行为和闭包的 原理: 执行上下文和作用域链是解释变量提升,this指向以及闭包如何"记住"其此法作用域的底层核心理论
3.调试和性能优化的基础: 理解执行上下文的创建和销毁过程(执行栈),以及作用域链的查找机制,有助于分析代码的内存使用情况(如闭包导致的内存泄漏)和性能瓶颈
十.js中的this到底指向谁
this是js中的一个关键字,它在执行上下文中是一个特殊的对象引用,与词语作用域不同,this的值是在函数被调用时才确定的(运行时绑定),而不是在函数定义时,它的指向完全取决于函数的调用方式
1.默认绑定
在全局作用域中
console.log(this);
在全局执行上下文中,this指向新创建的实例对象
2.普通函数调用
function test() {
console.log(this);
}
test(); // 浏览器中输出:window
普通函数调用,非严格模式下this指向全局对象(浏览器中是window)
严格模式下('user strict'),this是undefined
3.对象方法调用
const obj = {
name: 'wufeng',
say() {
console.log(this.name);
}
};
obj.say(); // 输出:'wufeng'
谁调用函数,this就指向谁,这里this指向obj
4.构造函数中
function Person(name) {
this.name = name;
}
const p = new Person('张三');
console.log(p.name); // 张三
构造函数中,this指向新创建的实例对象
当使用new 关键字调用一个函数(构造函数)时,会自动执行以下步骤:
- 创建一个全新的空对象
- 这个新对象的[[Prototype]]被链接到构造函数的prototype
- 这个新对象被绑定为函数调用的this
- 如果函数没有返回其它对象,则new表达式会隐式返回这个新对象
5.箭头函数
const obj = {
name: 'wufeng',
say: () => {
console.log(this.name);
}
};
obj.say(); // 输出:undefined(或全局 this.name)
箭头函数没有自己的this.它的this继承自外层作用域
6.手动绑定
通过call( ), apply( ), 或bind( )方法,可以明确地指定函数调用时的this值
- call(thisArg,arg1,arg2,...): 立即调用函数,this绑定到thisArg,参数逐个传递
- apply(thisArg,[argsArray]): 立即调用函数,this绑定到thisArg,参数以数组形式传递
- bind(thisArg): 不立即调用函数,而是返回一个this被永久绑定到thisArg的新函数
十一. 闭包到底是什么? 如何判断?
1.什么是闭包?
闭包指的是一个函数能够记住并访问它的词法作用域,即使这个函数在其词法作用域之外执行,它允许一个函数访问并操作函数外部的变量.
function outer() {
const name = 'wufeng';
return function inner() {
console.log(name); // inner 能访问 outer 的变量
};
}
const fn = outer(); // outer 执行完了
fn(); // 输出:wufeng(说明闭包存在)
2.为什么需要闭包?
本质原因:
因为js中函数执行完后,按理说它的局部变量会被销毁,但如果我们希望这些变量在函数执行后依然被访问或保留,那就需要闭包.
1.变量私有化(封装): 闭包可以创建不会被外部直接访问的私有变量
javascript
function createCounter(){
let count=0;
return function(){
count ++ ;
return count;
};
}
const counter = createCounter();
counter(); //1
counter(); //2
- count 是私有的,外部无法直接修改,只能通过闭包访问
- 相当于实现了封装和数据保护
2.维持状态(记住变量值)
function remember(value) {
return function () {
return value;
};
}
const fn = remember('wufeng');
fn(); // 永远返回 'wufeng'
普通函数执行完变量会消失,而闭包可以记住某次执行时的变量值
3.延迟执行时保留环境(如: 定时器,事件,异步等)
function delayLog(msg) {
setTimeout(function () {
console.log(msg); // msg 来自闭包
}, 1000);
}
delayLog('hello');
4.for循环一步陷阱解决
for (var i = 0; i < 3; i++) {
//每次循环调用函数
(function (j) {
//异步回调函数,内部函数访问了外部的参数j,当时外层函数其实早就执行完了
J被保留了下来, 正常来说,j应该随着外部函数执行完而销毁,但回调中还在访问j,所以js引擎通过闭包机制保留了j的值
setTimeout(() => console.log(j), 1000);
})(i);
}
// 输出 0 1 2,而不是 3 3 3
闭包保存每次循环的i值
闭包让函数记住它出生时的环境,实现变量私有化,状态保存,延迟访问等功能,是js中封装和异步编程的重要工具
3.如何形成闭包
闭包的形成条件是: 一个函数在其语法作用域之外被调用,但它依然访问了外部函数的变量
闭包的形成步骤:
- 在一个函数内部定义一个函数
- 外部函数执行并返回这个内部函数
- 外部函数执行完之后,内部函数仍访问外部变量