深浅拷贝
const value = { number: 10 };
const multiply = (x = { ...value }) => {
console.log((x.number *= 2))
}
multiply();
multiply();
multiply(value);
multiply(value);
// 20 20 20 40
第一次和第二次调用 multiply 函数时:
因为调用函数时没有传入参数,所以函数参数 x 会使用默认值。这个默认值是通过 {...value } 这种对象展开语法(浅拷贝)创建的新对象,每次调用都会重新创建这么一个独立的新对象,它的初始属性值来源于 value 对象,但后续对其操作不会影响到 value 本身,这两次调用函数内部操作的都是各自独立的新对象,走的就是默认值对应的逻辑。
第三次和第四次调用 multiply 函数时:
明确传入了 value 作为参数,此时函数内部的 x 就直接指向了传入的这个 value 对象了,函数里对 x 的操作就等同于直接对外部传入的 value 对象进行操作,不再是基于默认值去创建新对象然后操作了,所以这两次走的是传入外部 value 作为 x 参数的逻辑。
reduce函数
[1, 2, 3, 4].reduce((x, y) => console.log(x, y));
[1, 2, 3, 4].reduce((x, y) => console.log(x, y), 0);
[1, 2, 3, 4].reduce((x, y) => {
console.log(x, y)
return x + y
}, 0)
语法:
reduce(callbackFn)
reduce(callbackFn, initialValue)
回调函数(callbackFn)的参数
accumulator(累加器) :它是累计的结果,是上一次调用回调函数时返回的累积值或者是初始值(如果提供了的话)。在求和的例子中,它就像是一个不断累加的总和。currentValue(当前值) :它是当前正在处理的数组元素。在每次调用回调函数时,currentValue会从数组中按顺序取出一个元素。currentIndex(可选的当前索引) :它表示当前元素在数组中的索引位置。这个参数是可选的,如果在回调函数中不需要知道元素的位置,就可以忽略它。
initialValue可选
- 第一次调用回调时初始化
accumulator的值。如果指定了initialValue,则callbackFn从数组中的第一个值作为currentValue开始执行。如果没有指定initialValue,则accumulator初始化为数组中的第一个值,并且callbackFn从数组中的第二个值作为currentValue开始执行。在这种情况下,如果数组为空(没有第一个值可以作为accumulator返回),则会抛出错误。
第一个reduce函数调用,没有初始值,有返回值,只是返回值是console.log函数的返回值,而console.log函数本身返回undefined。所以第一次x赋值为数组的第一个值为1,y赋值为数组的第二个值为2,第二次x为undefined,y为数组的第三个值为3,依次类推,最终输出为
1 2
undefined 3
undefined 4
第二个reduce函数调用,有初始值,只是返回值是console.log函数的返回值,而console.log函数本身返回undefined。所以第一次x赋值为0,y赋值为数组的第一个值为1,第二次x为undefined,y为数组的第二个值为2,依次类推,最终输出为
0 1
undefined 2
undefined 3
undefined 4
第三个reduce函数调用,有初始值,且有返回值,所以第一次x赋值为0,y赋值为数组的第一个值为1,有返回值x+y,所以第二次x为第一次调用回调函数返回的值,即第一次x+y的值为1,y为数组的第二个值为2,依次类推,最终输出为
0 1
1 2
3 3
6 4
注意:console.log 函数本身返回 undefined
解构赋值
(1)
const { firstName: myName } = { firstName: 'Lydia' };
console.log(myName);
console.log(firstName);
对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
这里firstName是属性,而myName是一个变量,它的值作为firstName的属性值,myName被赋值为了Lydia,第一次输出Lydia,但是第二次输出firstName,firstName这个变量根本未定义,所以报错ReferenceError: firstName is not defined
(2)
const getList = ([x, ...y]) => [x, y]
const getUser = user => { name: user.name, age: user.age }
const list = [1, 2, 3, 4]
const user = { name: "Lydia", age: 21 }
console.log(getList(list))
console.log(getUser(user))
调用getList函数,根据解构赋值,输出[1, [2, 3, 4]]
调用getList函数,这里需要注意,箭头函数使用了 {} ,箭头函数中,如果函数体部分使用花括号 {} 包裹,那么它被视为一个代码块,在代码块内的语句就需要遵循语句的规范,比如需要使用 return 关键字来返回值(如果有返回值的话)。
此时里面的语句应该遵循代码代码块的规范,很明显语句不符合规范,所以报错ReferenceError,如果这个箭头函数想返回一个对象,需要改写成如下这种形式
const getUser = user => {
return {
name: user.name,
age: user.age
}
}
Promise
(1)
const myPromise = () => Promise.resolve('I have resolved!');
function firstFunction() {
myPromise().then(res => console.log(res));
console.log('second');
}
async function secondFunction() {
console.log(await myPromise());
console.log('second')
};
firstFunction();
secondFunction();
输出:
second
I have resolved!
I have resolved!
second
(2)
// A.js
export async function initialize() {
console.log('Initializing module A');
}
console.log('Module A loaded');
// B.js
import { initialize } from './A.js'
console.log('Module B loaded');
initialize().then(() => { console.log('Module A initialized from B') });
//index.js
import { initialize } from './A.js'
import './B.js'
console.log('Index module loaded');
initialize().then(() => { console.log('Module A initialized from index') })
执行index.js,执行结果:
(1)初始加载 index.js:
- 加载
index.js,同步执行,遇到import时按依赖顺序加载模块。
(2)加载 A.js:
-
A.js首次加载,以下内容依次执行:- 打印:
Module A loaded。 - 定义
initialize函数(但未执行)。
- 打印:
-
同步任务执行完毕,
A.js加载完成。
(3) 加载 B.js:
-
B.js加载时,导入A.js中的initialize(此时从缓存中取出,无需重新执行A.js的顶层代码)。 -
打印:
Module B loaded。 -
调用
initialize:initialize返回一个Promise,其.then方法将回调函数加入微任务队列。- 打印:
Initializing module A(这是同步任务)。
(4)返回到 index.js:
-
B.js加载完成后,返回到index.js:-
打印:
Index module loaded。 -
再次调用
initialize:initialize返回一个新的Promise,其.then方法的回调函数再次加入微任务队列。- 打印:
Initializing module A(第二次调用,属于同步任务)。
-
(5)执行微任务队列:
-
主线程同步任务执行完毕后,开始处理微任务:
-
执行第一个微任务(
B.js的then回调):- 打印:
Module A initialized from B。
- 打印:
-
执行第二个微任务(
index.js的then回调):- 打印:
Module A initialized from index。
- 打印:
-
注意知识点:
(1)async 函数的返回值是一个 Promise 对象
-
返回值:
-
async函数的返回值是一个Promise对象。 -
即使函数内部没有显式返回
Promise,它也会自动包装成Promise。- 如果
async函数显式返回值,例如return value,相当于返回Promise.resolve(value)。 - 如果抛出异常,则返回
Promise.reject(error)。
- 如果
-
-
内部
await的作用:await用于等待一个Promise的结果,当前async函数会暂停执行,直到该Promise完成。- 不含
await的async函数相当于一个立即返回Promise.resolve()的函数。
-
then的执行:- 调用
async函数后,可以链式调用.then,处理Promise的结果。
- 调用
(2)ES6 模块加载机制
1. 模块的加载:
- 当你使用 `import` 导入一个模块时,无论你只导入了模块的某个部分(比如一个函数 `initialize`),整个模块的顶层代码都会被执行一次。
2. 模块的顶层代码:
所谓顶层代码,是指模块中直接书写在 export 或函数之外的代码。
在 A.js 中,这段代码:
console.log('Module A loaded');
就是顶层代码。
3. 模块的执行与缓存:
- 第一次加载:当模块第一次被导入时,所有顶层代码都会执行,且模块的导出内容会被缓存。
- 后续加载:如果该模块再次被导入,则直接使用缓存,不会再次执行顶层代码。
(3)
Promise.resolve().then(() => {
console.log(0)
return Promise.resolve(4)
}).then((res) => {
console.log(res);
})
Promise.resolve().then(() => {
console.log(1)
}).then(() => {
console.log(2);
}).then(() => {
console.log(3);
}).then(() => {
console.log(5);
}).then(() => {
console.log(6);
})
首先对return Promise.resolve(4)进行拆解
Promise.resolve().then(() => {
console.log(0)
return Promise.resolve(4)
})
会被转化为
Promise.resolve().then(() => {
console.log(0)
return 4
}).then().then().then((res) => {
console.log(res)
})
分析执行顺序:
1. 初始化主线程任务
-
两个
Promise.resolve()的第一个.then被加入到微任务队列中,按照书写顺序:- 第一个
Promise.resolve的console.log(0)。 - 第二个
Promise.resolve的console.log(1)。
- 第一个
2. 第一次微任务队列执行
按顺序执行:
第一个 .then(第一组 Promise 的 console.log(0)):
- 打印:
0。 - 返回
4,将其.then回调加入微任务队列。
第二个 .then(第二组 Promise 的 console.log(1)):
- 打印:
1。
此时微任务队列新增任务:
- 第一组
return 4的.then回调。 - 第二组的下一个
.then回调:console.log(2)。
3. 第二次微任务队列执行
按顺序执行:
第一组 .then 回调(then()):
第二组 .then 回调(console.log(2)):
- 打印:
2。
此时微任务队列新增任务:
- 第一组
.then的.then回调。 - 第二组的下一个
.then回调:console.log(3)。
4. 第三次微任务队列执行
按顺序执行:
第一组 .then 回调(then()):
第二组 .then 回调(console.log(3)):
- 打印:
3。
此时微任务队列新增任务:
- 第一组
.then的console.log(4)回调。 - 第二组的下一个
.then回调:console.log(5)。
5. 第四次微任务队列执行
按顺序执行:
第一组 .then 回调(console.log(4)):
- 打印:
4。
第二组 .then 回调(console.log(5)):
- 打印:
5。
此时微任务队列新增任务:
- 第二组的下一个
.then回调:console.log(6)。
6. 第五次微任务队列执行
按顺序执行:
第二组 .then 回调(console.log(6)):
- 打印:
6。
最终结果:0 1 2 3 4 5 6
注意知识点:
(1)同一批次的 Promise 回调(.then())会按顺序加入微任务队列并执行。每个 Promise 中的 .then() 都会被加入队列,执行顺序遵循它们被注册的顺序。
跳过的 .then() :如果 .then() 没有回调函数,它会跳过但仍会将值传递给下一个 .then(),所以它们依然会加入微任务队列。
(2)当 then() 方法没有参数时,它的行为是跳过当前 .then() 并直接将其上一个 .then() 传递下去,返回一个新的 已解决的 Promise,并且该 Promise 会继续链式传递值。
具体来说,当 .then() 没有传入回调函数时:
- 它会自动返回一个
Promise.resolve(),并将前一个.then()的返回值传递给下一个.then()。 - 相当于该
.then()什么都不做,只是将值原封不动地传递下去。
普通函数和箭头函数
(1)经典Foo函数问题1
var getName = () => { alert(4); }
function getName() { alert(5); }
getName()
分析:考察变量提升
即所有声明变量或声明函数都会被提升到当前函数的顶部。
console.log(x);//输出:function x(){}
var x=1;
function x(){}
这个会被转化为
var x
function x() { }
console.log(x);//输出:function x(){}
x = 1;
所以输出的是function x(){},x最终会被覆盖为1
所以上面的getName()的调用结果为弹出4,因为getName最终被覆盖为() => { alert(4);
(2)经典Foo函数问题2
function Foo() {
getName = function () { alert (1); };
return this;
}
Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
var getName = function () { alert (4);};
function getName() { alert (5);}
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
具体详细回答见www.cnblogs.com/xxcanghai/p…
结果:2 4 1 1 2 3 3
第三个弹出2,为啥?
- 首先执行
Foo(),在Foo函数内部,有getName = function () { console.log(1); };这行代码,这里的getName没有使用var等关键字声明,它实际上是在全局作用域下隐式地创建了一个全局变量(或者说是修改了已经存在的getName变量,如果有的话),并将其赋值为一个新的函数,这个函数执行时会输出1。 - 然后
Foo函数返回了this,因为是全局调用,所以this指向全局对象(在浏览器环境中就是window对象)。由于前面的操作已经把全局的getName修改为新的函数了,所以当通过返回的this(也就是全局对象)去调用getName方法时,实际上调用的就是刚刚被重新赋值的那个会输出1的函数,因此这里会输出1。
隐式全局变量
当在函数内部,直接给一个没有使用 var、let、const 等关键字声明的变量赋值时,JavaScript 会把这个变量当作全局变量来处理(如果这个变量名在全局作用域中不存在,就会创建它;如果已经存在,就会修改它的值)但是只有在调用函数,执行到函数内部那段代码时,才会创建隐式的全局变量,这也就是为啥第二问是输出的4
第五个弹出2,为啥?
这里涉及到运算符优先级的问题,new 操作符和点号(.)操作符同时出现时,点号(.)的优先级更高,所以代码实际执行顺序相当于 new (Foo.getName)();,也就是先获取 Foo 函数对象上的 getName 静态方法,然后使用 new 操作符去调用这个方法构造一个新对象。而 Foo.getName 对应的方法执行时会弹出 2,所以这里弹出 2。
第七个弹出3,为啥?
new new Foo().getName(); 的执行顺序就是先通过 new Foo() 创建一个实例对象,然后在实例对象的原型链上找到 getName 方法,最后再用外层的 new 操作符基于这个 getName 方法来创建一个新对象,而 getName 方法执行时弹出 3,所以最终整体的执行结果就是弹出 3 。可以理解为执行顺序是new (new Foo().getName)()
new操作符的行为
- 当使用`new`关键字来调用一个函数(在这个例子中是`Car`函数)时,它会经历以下几个步骤:
- 创建一个新的空对象。
- 将这个新对象的`__proto__`属性(在 ES6 的规范中是`[[Prototype]]`内部属性)设置为构造函数(这里是`Car`)的`prototype`属性。
- 将这个新对象作为`this`上下文传递给构造函数,并执行构造函数。
- 如果构造函数没有显式地返回一个非原始值(对象、函数、数组等),那么`new`表达式的值就是这个新创建的对象。
这两种写法是等价的
function Car() {
console.log(1);
}
var car1 = new Car; // 虽然没有在函数名后面加括号`()`,但是 JavaScript 会自动在内部为你调用这个函数,就好像你写了`var car1 = new Car();`一样。
var car2 = new Car();
React中的setState
import React, { Component } from 'react'
export default class App extends Component {
constructor() {
super()
this.state = {
val: 0
}
}
componentDidMount() {
this.setState({ val: this.state.val + 1 })
console.log('第一次', this.state.val)
// 第 1 次 log
this.setState({ val: this.state.val + 1 })
console.log('第二次', this.state.val)
// 第 2 次 log
this.setState({ val: this.state.val + 1 })
console.log('第三次', this.state.val)
// 第 3 次 log
}
render() {
return (
<div>{ this.state.val }</div>
)
}
}
关键点分析:
-
componentDidMount是一个生命周期方法,它会在组件挂载后立即执行,因此所有的setState调用将在组件加载完毕后触发。 -
setState的异步性:setState是异步的,每次调用setState并不会立即更新this.state,而是将状态更新请求排入队列,等到事件处理函数执行完毕之后,React 才会批量更新状态。- 在
componentDidMount方法中,每次调用setState后,console.log打印的this.state.val依然是更新前的值,因为 React 还没有执行状态更新。
-
this.state.val是闭包中的值:- 在
componentDidMount执行过程中,每次setState调用后,console.log打印的是当前事件循环中的值,而不是更新后的值。 - 由于
setState是异步的,所以每次调用setState后,this.state.val依然是更新前的值,直到 React 批量更新并重新渲染组件。
- 在
执行顺序:
- 第一次
setState调用:
-
this.state.val初始值是0。 -
调用
this.setState({ val: this.state.val + 1 })后,React 将这个更新请求排入更新队列,但不会立即更新this.state.val。 -
此时
console.log('第一次', this.state.val)打印的还是0,因为更新还未完成。
- 第二次
setState调用:
-
同样,调用第二次
setState时,this.state.val依然是0(React 尚未完成第一次的状态更新)。 -
console.log('第二次', this.state.val)打印的仍然是0。
- 第三次
setState调用:
- 同样地,第三次调用
setState时,this.state.val依然是0(React 尚未完成前两次的状态更新)。 console.log('第三次', this.state.val)打印的仍然是0。
解释为什么 div 中显示 1
React 批量更新并重新渲染:
- React 将所有
setState合并成一次更新,并且会在下一轮渲染时应用状态更新。 - 但是,React 只有在所有
setState调用完成后才会重新渲染组件。因此,组件会在状态更新完成后重新渲染,<div>中显示的val是1,而不是3,这是因为在 React 内部处理setState时,会将多个更新合并成一次更新,并根据最新的合并状态值来渲染组件。
如何避免
import React, { Component } from 'react';
export default class App extends Component {
constructor() {
super();
this.state = {
val: 0
};
}
onClick = () => {
this.setState(prevState => ({ val: prevState.val + 1 }));
console.log('第一次', this.state.val); // 打印的是当前状态值
this.setState(prevState => ({ val: prevState.val + 1 }));
console.log('第二次', this.state.val); // 打印的是当前状态值
this.setState(prevState => ({ val: prevState.val + 1 }));
console.log('第三次', this.state.val); // 打印的是当前状态值
}
render() {
return (
<div onClick={this.onClick}>{this.state.val}</div>
);
}
}
使用函数式 setState
函数式 setState 允许你基于前一个状态值来计算新的状态。React 会自动传递当前的状态值(即 prevState),你可以在更新中直接使用这个值,而不是依赖于外部的 this.state 值。
-
prevState => ({ val: prevState.val + 1 }):每次调用setState时,都会传入一个回调函数,该回调函数接收prevState(前一个状态)作为参数,使用它来计算下一个状态。这样就可以确保每次更新都基于最新的状态,而不是基于当前this.state.val。 -
这样,即使
setState是异步的,你每次都能确保基于最新的状态更新,而不会受到闭包的影响。
为什么这种方法有效?
- 异步状态更新:因为
setState是异步的,React 会将多次更新合并成一个批量更新,而你使用函数式更新时,React 会在每次setState调用时都根据最新的prevState来计算新的状态值。 - 避免闭包问题:函数式更新确保了每次
setState都是基于上一个更新后的最新状态,而不是基于事件处理函数开始时的状态值,避免了闭包导致的状态值不一致的问题。
setState同步执行
class App extends React.Component {
constructor() {
super()
this.state = {
val: 0
}
}
onClick = () => {
setTimeout(() => {
this.setState({ val: this.state.val + 1 })
console.log('第一次', this.state.val)
this.setState({ val: this.state.val + 1 })
console.log('第二次', this.state.val)
this.setState({ val: this.state.val + 1 })
console.log('第三次', this.state.val)
}, 0)
}
render() {
const { val } = this.state
console.log('组件渲染了,count的值为' + val)
return <div onClick={this.onClick}>{val}</div>
}
}
export default App
当第一次执行了onClick函数的时候,组件就会重新渲染,这意味着在setTimeout中的setState函数仍然还是同步执行的,但是在执行完setState函数之后,当前作用域中的count并没有发生改变,所以在当前作用域下获取count的值仍然为旧的状态,但是在新渲染的组件作用域中就可以访问到最新的状态,count打印出来是1;
对象作为键
let a = {}
let b = { key: 'b' }
let c = { key: 'c' }
a[b] = 123
a[c] = 456
console.log(a[b]);
在JavaScript中,对象属性的键本质上是字符串或Symbol类型。当使用对象作为属性名时,会通过toString()方法隐式转换为字符串"[object Object]"。因此,代码中a[b]和a[c]实际上访问的是同一个键"[object Object]",导致后者覆盖前者:
let a = {};
let b = { key: 'b' };
let c = { key: 'c' };
// 以下两行等价于:
// a["[object Object]"] = 123;
// a["[object Object]"] = 456;
a[b] = 123;
a[c] = 456;
console.log(a[b]); // 输出456(因为键相同,后赋值覆盖前值)
关键点解析:
对象转字符串:b.toString()和c.toString()的结果均为"[object Object]",导致属性名冲突
属性覆盖机制:后设置的属性值会覆盖同键的旧值
运算符转换
console.log(+'1' + '2' + '1');
console.log('A' - 'B' + '1');
根据JavaScript的类型转换规则,两个表达式的输出结果如下:
console.log(+'1' + '2' + '1'); // 输出: 121
console.log('A' - 'B' + '1'); // 输出: NaN1
第一个表达式:+'1' + '2' + '1'
- 一元加操作符(+):第一个操作数是+'1',这里的加号是一元操作符,会将字符串'1'转换为数值1。因此,表达式变为
1 + '2' + '1'。 - 字符串拼接:在JavaScript中,当加法操作中有一个操作数是字符串时,整个表达式会执行字符串拼接。因此,
1 + '2'会变成字符串 '12',接着再加上 '1',结果为 '121'。
所以,第一个表达式的输出应该是 '121'。
第二个表达式:'A' - 'B' + '1'
- 减法操作符(-):第一个操作数是'A',第二个是'B'。减法操作符会尝试将操作数转换为数值。由于'A'和'B'无法转换为有效的数值,结果为
NaN。 - 加法操作:接下来,
NaN + '1'。此时,加法操作符的一边是NaN,另一边是字符串 '1'。根据规则,如果有一个操作数是字符串,整个表达式会执行字符串拼接。因此,NaN会被转换为字符串 'NaN',结果为 'NaN1'。
所以,第二个表达式的输出应该是 'NaN1'。
定时器setInterval偏差
const startTime = new Date().getTime(); // 1000
let count = 0;
const interval = setInterval(function () {
count++
console.log(new Date().getTime() - (startTime + count * 1000) + 'ms')
if (count == 10) {
clearInterval(interval);
}
}, 1000)
输出结果:
可以看到,每次打印的输出结果都是随机的
具体原因及分析:
一、单线程与任务队列的阻塞
JavaScript 是单线程的,所有同步任务和异步回调(包括 setInterval)都在同一线程中排队执行。当主线程被其他任务(如 UI 渲染、复杂计算、网络请求等)占用时,setInterval 的回调会被推迟到主线程空闲时执行。例如:
- 长任务阻塞:如果某次回调函数内部有耗时操作(如循环或 DOM 操作),后续回调会积压在任务队列中,导致实际执行时间远落后于预期
- 异步任务延迟:即使回调本身不耗时,主线程其他任务的执行也会占用时间,导致
setInterval的回调无法按时触发。
二、setInterval 的固有缺陷
setInterval 并非“精准计时器”,而是周期性将回调推入任务队列。其设计机制导致以下问题:
-
任务堆积:
如果回调执行时间超过设定的间隔(如 1000ms),下一次回调会被直接加入队列,导致两次回调之间的间隔小于预期,甚至出现连续执行的情况。 -
误差累积:
每次回调的实际执行时间与理想时间的差值会逐渐积累。例如: 每次回调的实际执行时间与理想时间的差值会逐渐积累。例如:javascript // 假设某次回调因主线程阻塞延迟了 200ms // 下一次回调的触发时间仍按 1000ms 计算,而非根据实际执行时间调整
三、浏览器优化与限制
-
最小延迟限制:
现代浏览器对setInterval的最小延迟有隐性限制(通常为 4ms)。若设置时间小于 4ms(如setInterval(fn, 0)),实际延迟会被强制调整为 4ms。 -
后台标签页降频:
如果页面处于非活动状态(如切换到其他标签页),浏览器会降低定时器执行频率(最低至 1 秒一次),以节省资源。 -
系统调度影响:
操作系统和硬件的性能差异(如 CPU 负载、电池节能模式)也会导致定时器触发时间不稳定。
四、代码示例中的具体表现
代码中,console.log 输出的时间差是 当前时间与预期时间(startTime + count * 1000)的偏差。由于上述原因,每次回调的实际触发时间会与理想时间存在不可控的偏移,具体表现为:
- 首次回调可能接近准确(如延迟 10ms)。
- 后续回调的延迟逐渐增大(如 50ms → 200ms → 500ms),甚至因主线程阻塞出现跳跃式增长