JavaScript-Advanced Days1 Notes
1、为什么如下代码,最后一行的i打印不出,但是第1个循环外面的i却能够打印出来?
<script> for (var i = 1; i <= 3; i++) { // 块作用域 console.log(i) } console.log(i) for (let i = 1; i <= 3; i++) { // 块作用域 console.log(i) } for (let i = 1; i <= 3; i++) { // 块作用域 console.log(i) } if (true) { let i = 10 } console.log(i) </script>
在这段代码中,var 和 let 的作用范围不同,这导致了最后一行的 i 打印不出来,而第一个循环外的 i 却可以打印出来。
关键点在于 var 和 let 的作用域:
-
var是函数作用域或全局作用域:- 当你使用
var声明变量时,即使它是在for循环中或if语句块中声明的,变量仍然会在整个函数或全局范围内生效。因此,即使i是在for循环中声明的,它在循环外依然是可访问的。
- 当你使用
-
let是块作用域:let声明的变量具有块作用域。也就是说,变量仅在当前的代码块{}内有效。一旦离开该块,变量就不再可用。
总结:
var声明的变量具有全局或函数作用域,因此即使在块中声明,变量在块外仍然可以访问。let声明的变量具有块作用域,因此在块(例如for循环或if语句)外无法访问。
因此,最后一行打印 i 时,由于使用的是 let 声明的变量,超出了它的作用域,导致无法打印。而使用 var 时,变量 i 的作用域是全局的,因此在循环外仍然可以访问并打印。
2、如何理解下面代码,用垃圾回收机制的角度
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script> for (let i = 1; i <= 3; i++) { } let num = 10 function fn() { const str = 'andy' // str = 'lily' console.log(str) } fn() fn() fn() </script> </body> </html>
在这段代码中,垃圾回收机制的作用是自动管理内存,释放那些不再被引用或使用的对象。浏览器的垃圾回收器会定期查找那些没有引用的变量和对象,并释放它们占用的内存空间。
垃圾回收机制概述
JavaScript 使用的是自动垃圾回收机制,最常用的算法是标记清除(Mark-and-Sweep)。当变量或对象不再被引用时,垃圾回收器会将其标记为"可回收",随后会清除这些不再使用的变量,释放内存。
如何在你的代码中发挥作用:
-
for循环中的变量i:- 在循环中使用了
let i = 1; i <= 3; i++,由于let是块作用域的,i在循环结束后会被回收。 - 当
for循环结束时,i变量不再可用,因此它的内存会被垃圾回收机制自动释放。
- 在循环中使用了
-
num = 10:- 变量
num被声明并赋值为 10,它是在全局作用域中声明的,因此在脚本运行时会一直存在,除非显式地重新分配或者覆盖它。
- 变量
-
函数
fn和局部变量str:- 每次调用函数
fn()时,都会创建一个新的局部变量str,并在函数执行结束后这个局部变量将不再被引用。 - 垃圾回收器在函数
fn执行结束后会标记str变量为可回收对象,并释放其内存。 - 当
fn()被调用三次时,虽然每次都会创建新的str变量,但每次函数调用结束后,之前的str都会被垃圾回收,不会造成内存泄漏。
- 每次调用函数
-
多次调用函数
fn():- 每次调用函数
fn()都会创建独立的上下文环境,其中包含局部变量str。一旦函数调用结束,局部变量str就会失效,随后会被垃圾回收。 - 由于变量
str在每次调用时都是局部的且不会被外部引用,所以它在函数结束后可以安全地回收。
- 每次调用函数
垃圾回收机制作用总结:
for循环的i在块结束后就不再可访问,因此可以回收。- 局部变量
str在每次fn函数执行结束后失效,并被回收。 - 全局变量
num一直存活,因为它没有被覆盖或者释放。
垃圾回收机制的关键是,它会自动释放那些不再被使用或引用的变量,避免内存泄漏,确保程序的内存占用始终处于合理状态。
3、如何理解如下代码,变量提升的概念
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script> // 1. 把所有var声明的变量提升到 当前作用域的最前面 // 2. 只提升声明, 不提升赋值 var num console.log(num + '件') num = 10 console.log(num) function fn() { console.log(num) var num = 10 } fn() </script> </body> </html>
变量提升的概念与作用
变量提升(Hoisting) 是 JavaScript 中的一个机制,在代码执行之前,变量和函数声明会被移动到其所在作用域的顶部。注意,只有声明被提升,赋值并不会被提升。只有var有变量提升,但这其实不好事,是JS的一大缺陷。
变量提升有两个关键点:
- 声明被提升:JavaScript 引擎会把变量和函数的声明提升到当前作用域的顶部(无论是全局作用域还是函数作用域)。
- 赋值不提升:虽然声明会被提升,但变量赋值依然发生在代码运行时的原位置。

代码分析
var num; console.log(num + '件'); // 输出: undefined件 num = 10; console.log(num); // 输出: 10
1. 变量 num 的提升
- 第一行:
var num实际上被提升到了代码块的顶部。 console.log(num):因为声明已经被提升,但赋值还没有发生,因此此时num的值是undefined,输出undefined件。num = 10:此时num被赋值为10,然后下一行输出10。
变量提升的实质是,var num; 会被看作是在代码的最开始定义,但没有赋值。
2. 函数内的变量提升
function fn() { console.log(num); var num = 10; } fn();
var num = 10在函数作用域内声明,函数内的var num会被提升到函数的顶部。- 实际执行过程类似于:
function fn() { var num; // 提升了声明 console.log(num); // undefined,因为此时还没有赋值 num = 10; // 在这时才进行赋值 }
- 因此
console.log(num)输出的是undefined,而不是全局变量num的值。
通俗解释
变量提升就像是在你编写的代码中,所有用 var 声明的变量,都会被“偷偷地”提到代码的最上面,但只有声明会被提上去,赋值不会。
举例说明
console.log(a); // 输出:undefined var a = 5; console.log(a); // 输出:5
这段代码的执行顺序实际上是这样的:
var a; console.log(a); // 输出:undefined a = 5; console.log(a); // 输出:5
为什么有变量提升?
变量提升的设计可以让 JavaScript 在解释执行代码时,不会因为变量的声明顺序而报错。这给了开发者在写代码时更大的灵活性。
总结
- 变量提升 是 JavaScript 的机制,
var声明的变量会被提升到作用域的最顶端。 - 只有声明被提升,赋值留在原位置。
- 函数内的变量声明同样会提升,但赋值不会影响提升前的行为。
4、如下代码,为什么要加上...才能返回对应的最大、最小值、数组合并呢?
<script> const arr1 = [1, 2, 3] // 展开运算符 可以展开数组 // console.log(...arr) // console.log(Math.max(1, 2, 3)) // ...arr1 === 1,2,3 // 1 求数组最大值 console.log(Math.max(...arr1)) // 3 console.log(Math.min(...arr1)) // 1 // 2. 合并数组 const arr2 = [3, 4, 5] const arr = [...arr1, ...arr2] console.log(arr) </script>
关于 ... 展开运算符(spread operator)
在 JavaScript 中,... 被称为展开运算符,它的作用是将数组或对象“展开”为单独的元素或属性。这在函数调用、数组操作和对象合并等场景中非常有用。
为什么要加 ... 才能返回最大、最小值?
示例代码:
const arr1 = [1, 2, 3]; console.log(Math.max(...arr1)); // 3 console.log(Math.min(...arr1)); // 1
理解:
-
Math.max()和Math.min()这两个函数需要一组独立的数值作为参数,而不是数组。如果直接传入数组(比如Math.max([1, 2, 3])),它不会工作,因为这些函数期望接收到一组单独的数值,而不是一个数组。 -
...arr1是展开运算符,它将数组arr1中的元素[1, 2, 3]展开成独立的数值,等同于Math.max(1, 2, 3)。这就是为什么要加...,它能够将数组展开成独立的参数传递给函数。
具体说明:
Math.max(...arr1)会将数组[1, 2, 3]展开为1, 2, 3,并返回其中的最大值3。Math.min(...arr1)则返回最小值1。
为什么要加 ... 来合并数组?
示例代码:
const arr2 = [3, 4, 5]; const arr = [...arr1, ...arr2]; console.log(arr); // [1, 2, 3, 3, 4, 5]
理解:
...展开运算符在数组合并时非常方便,它将arr1和arr2中的元素展开为独立的元素,并通过[...]将这些展开的元素重新组合成一个新的数组。[...arr1, ...arr2]的效果是将两个数组展开并合并,等同于[1, 2, 3, 3, 4, 5]。
不使用展开运算符的替代方法:
你也可以使用 concat() 来合并数组:
const arr = arr1.concat(arr2); console.log(arr); // [1, 2, 3, 3, 4, 5]
不过,使用展开运算符通常更加简洁和现代。
总结:
- 展开运算符
...可以将数组中的元素“展开”为单独的值,这对于函数调用和数组合并等操作非常有用。 - 在使用
Math.max()和Math.min()时,展开运算符能够将数组中的元素作为单独的数值传递给函数。 - 在数组合并时,
...展开运算符使得合并更加简洁和直观。
5、为什么这个函数返回的是 {uname: 刘德华} 按道理直觉上理解不应该是返回 {刘德华: 刘德华}?
// 5. 箭头函数可以直接返回一个对象 const fn = (uname) => ({ uname: uname }) console.log(fn('刘德华'))
在你的代码中,函数返回的是 {uname: 刘德华},这是因为你定义的返回对象使用了属性名 uname,并将传入的参数 uname 的值赋给了该属性。以下是详细解释:
代码回顾
const fn = (uname) => ({ uname: uname }); console.log(fn('刘德华')); // {uname: '刘德华'}
详细说明
-
uname: uname语法:- 左边的
uname是对象的属性名。 - 右边的
uname是箭头函数的参数,即传入的值'刘德华'。
所以
{ uname: uname }实际上是一个对象字面量,表示对象的属性名是uname,属性值是'刘德华'。 - 左边的
-
简写形式: 如果对象的属性名和属性值的变量名相同,JavaScript 允许使用简写形式,省略右边的赋值变量:
const fn = (uname) => ({ uname });
这和 ({ uname: uname }) 是等效的,简化了代码书写。
为什么不是 {刘德华: 刘德华}?
如果你想要返回 {刘德华: '刘德华'},即将属性名动态地设置为参数的值,可以这样改写代码:
在这里,方括号 [] 表示属性名是动态的,[uname] 会将传入的值 '刘德华' 作为对象的属性名。
总结:
{ uname: uname }是在创建一个对象,其中属性名是uname,属性值是传入的参数'刘德华'。- 如果你想要动态地将参数值作为对象的属性名,需要使用
[uname]这种动态属性语法。
6、为什么如下代码,返回的结果中有个f, {name: 'andy', sayHi: ƒ}, 这个是什么?固定的表示吗?
// 对象方法里面的this const obj = { name: 'andy', sayHi: function () { console.log(this) // obj } } obj.sayHi()
在你的代码中,返回的结果 {name: 'andy', sayHi: ƒ} 中的 ƒ 并不是一种固定的表示法,而是浏览器开发工具(通常是 Chrome)用于简化显示函数的形式。
它表示对象中的某个属性是一个函数(function),具体而言,ƒ 是 "function" 的缩写,表示 sayHi 是一个方法(函数)。
详细说明
-
代码分析:
const obj = { name: 'andy', sayHi: function () { console.log(this) // obj } } obj.sayHi()
-
在这个对象
obj中,有两个属性:name: 'andy':这是一个字符串属性。sayHi: function () {...}:这是一个方法(即函数作为对象的属性)。
-
ƒ的含义: 当你在控制台查看对象时,浏览器将函数属性显示为ƒ以表明该属性是一个函数。例如,在{name: 'andy', sayHi: ƒ}中:name: 'andy'是普通的键值对。sayHi: ƒ意味着sayHi是一个方法(函数)。
实际上,
ƒ仅仅是浏览器(例如 Chrome)显示的一种简化符号。在代码层面,它依旧是标准的 JavaScript 函数。你可以执行obj.sayHi(),并且它会正确运行。 -
this的作用:- 当你调用
obj.sayHi()时,this在函数内部会指向调用它的对象obj。 - 所以当你
console.log(this)时,它会打印出整个obj对象,形如{name: 'andy', sayHi: ƒ},其中sayHi作为一个方法(函数)被简化显示为ƒ。
- 当你调用
示例:
在 Chrome 浏览器开发者工具中,你可能会看到类似这样的结果:
{name: 'andy', sayHi: ƒ}
表示对象 obj 有一个 name 属性,以及一个 sayHi 函数。
总结:
ƒ是浏览器对函数的一种简化表示。- 它表明对象中的属性
sayHi是一个函数。 - 这并不是 JavaScript 固定的表示法,而是调试工具中的简写。
7、关于如下代码为什么必须要加封号的理解。
<script>
// 1. 立即执行函数要加
(function () { })();
(function () { })();
// 2. 使用数组的时候/
const arr = [1, 2, 3]
const str = 'pink';
[1, 2, 3].map(function (item) {
console.log(item)
})
// 3. 解构赋值
let a = 1
let b = 2
;[b, a] = [a, b]
console.log(a, b)
</script>
通过具体的例子,详细解释加分号和不加分号的区别,特别是在 JavaScript 的自动分号插入 (ASI) 机制下。
例子 1:立即执行函数
不加分号的情况
var a = 1 (function() { console.log('立即执行函数') })()
如果你不加分号,JavaScript 会认为 (function() { console.log('立即执行函数') })() 是要和前面的 var a = 1 连接在一起。因此,它会试图将整个代码理解为这样:
var a = 1(function() { console.log('立即执行函数') })()
这就变成了:1 后面跟着立即调用一个函数,而这是非法的,因为 1 不是一个可以调用的函数。所以浏览器会报错。

加分号的情况
var a = 1; (function() { console.log('立即执行函数') })()
加了分号后,JavaScript 明确知道 var a = 1 这一句已经结束,接下来是一句新的代码 (function() { console.log('立即执行函数') })(),这才会正确执行。
不加分号的后果:代码语法错误,无法执行。
例子 2:数组作为新一行的开头
不加分号的情况
const arr = [1, 2, 3] [1, 2, 3].map(function(item) { console.log(item) })
JavaScript 在这里会误以为 [1, 2, 3] 是在跟前一行 arr 做某种操作,具体来说,JavaScript 会试图将 [1, 2, 3] 当作数组索引器来看,
也就是认为 [1, 2, 3] 是 arr[1] 这种格式中的一部分。这就导致代码运行时没有按预期打印出数组内容,而是可能会报错。
![]()
加分号的情况
const arr = [1, 2, 3]; [1, 2, 3].map(function(item) { console.log(item) })
加上分号后,JavaScript 就知道 const arr = [1, 2, 3] 已经结束,接下来 [1, 2, 3] 是一个独立的数组操作。这样就不会产生错误。
不加分号的后果:代码逻辑错误,结果不如预期。
例子 3:解构赋值
不加分号的情况
let a = 1 let b = 2 [b, a] = [a, b] console.log(a, b)
在这种情况下,JavaScript 会试图把 [b, a] 解释为前一行 let b = 2 的延续部分,就像它在做数组索引一样。例如,它可能会误以为你想这样写:
let b = 2[b, a] = [a, b]
显然这是不合法的,最终导致报错。

加分号的情况
let a = 1; let b = 2; [b, a] = [a, b]; console.log(a, b)
加上分号后,JavaScript 明确知道 let b = 2 这一句结束了,接下来 [b, a] = [a, b] 是一个独立的解构赋值操作。所以不会报错。
不加分号的后果:代码语法错误,无法执行。
总结:
- 不加分号时,JavaScript 有时候会把两行代码误解为同一个操作,导致语法错误或者逻辑错误。
- 加分号能避免这种误解,明确告诉 JavaScript 哪里是一句代码的结束,哪里是新的一句代码。
自动分号插入机制并不是总能正确地补上分号,所以在某些情况下,例如立即执行函数、数组操作或解构赋值时,必须手动加分号,以避免代码执行错误。
8、如下代码的.filter()方法如何理解?
// 2. 过滤筛选 document.querySelector('.filter').addEventListener('click', e => { // e.target.dataset.index e.target.tagName const { tagName, dataset } = e.target // 判断 if (tagName === 'A') { // console.log(11) // arr 返回的新数组 let arr = goodsList if (dataset.index === '1') { arr = goodsList.filter(item => item.price > 0 && item.price <= 100) } else if (dataset.index === '2') { arr = goodsList.filter(item => item.price >= 100 && item.price <= 300) } else if (dataset.index === '3') { arr = goodsList.filter(item => item.price >= 300) } // 渲染函数 render(arr)
filter() 是 JavaScript 数组中的一个方法,它用于 筛选数组中的元素,并根据条件返回一个新的数组。这个新数组只包含满足条件的元素。
filter() 方法的作用和工作方式:
-
作用:
filter()方法会遍历数组的每个元素,调用一个回调函数,检查每个元素是否符合条件。如果该元素符合条件(回调函数返回true),则将这个元素放入新数组中。最后返回一个包含所有符合条件元素的数组。 -
语法:
let newArray = array.filter(callback(element, index, array))
-
callback: 这是一个返回布尔值的函数,决定元素是否被保留。element: 当前正在处理的数组元素。index(可选):当前元素的索引。array(可选):原始数组。
newArray: 返回的新数组,包含所有通过callback函数筛选的元素。
通俗解释:
filter() 就像是在给数组中的每个元素做一个筛选测试,只有通过测试的元素才会被保留下来。
例如如下例子,理解鹈
<script> const arr = [10, 20, 30] // const newArr = arr.filter(function (item, index) { // // console.log(item) // // console.log(index) // return item >= 20 // }) // 返回的符合条件的新数组 const newArr = arr.filter(item => item >= 20) console.log(newArr) </script>

浙公网安备 33010602011771号