一、
二、
var a = 0,
b = 0
function A(a) {
A = function (b) {
console.log(a + b++)
}
console.log(a++) // 第一次执行到这的时候,变量a在内存中的值已经为2
}
A(1)
A(2)
// 1.首先,全局变量'a'和'b'被初始化为0
// 2.调用函数A(1),进入函数体内:
/**
* (1)传入参数 a 为1,
* (2)函数A被重新赋值为一个新的函数
* (3)在函数内部,打印输出a++,此时打印出的a为1,然后进行a++,在内存中a的值为2
*/
// 3.调用函数A(2),进入函数体内:
/**
* (1)A函数已被定义,此时执行的是新定义的函数 function(b)
* (2)传入b的值为2
* (3)在新定义的函数内部,计算并输出a+b++,其中a为之前的参数2,b是2(传入的参数)
* (4)打印出a+b++的结果为4,b再执行自增,最终b的值变成3
*/
/**
* (1)变量作用域和提升: JavaScript 中的变量有不同的作用域,包括函数作用域和全局作用域。
* 在函数内部重新定义变量会覆盖外部的同名变量。此外,变量和函数在执行之前会被提升到作用域的顶部。
*
* (2)函数声明和函数表达式: JavaScript 中可以使用函数声明和函数表达式来创建函数。函数声明会被提升,
* 而函数表达式则不会。在这个题目中,函数 A 被反复赋值,所以会导致不同的行为。
*
* (3)闭包: 在 JavaScript 中,函数可以访问其外部作用域中的变量。在这个题目中,函数 A 的内部函数(也叫闭包)
* 会保留对外部变量 a 的引用,即使 A 函数被重新赋值,闭包中的引用仍然存在。
*
* (4)参数传递和自增运算: 函数可以接受参数,并在内部进行操作。自增运算符 ++ 会增加变量的值。
*
* (5)函数调用和执行顺序: 函数按照调用的顺序依次执行,但由于 JavaScript 的事件循环机制,异步操作可能会导致某些代码在其他代码之前执行。
*/
三、
/**
* 1.'.'和'='的优先级
*/
let a = { n: 1 }
let b = a // 浅拷贝,a、b指向同一个堆内存,a、b引用同一个对象{n:1}
a.x = a = { n: 2 } // a = {n:2}
console.log(a.x)
console.log(b.x)
/**
* 这段代码涉及了JavaScript中的赋值、引用、以及操作符优先级等概念
*
* 第一步: let a = { n: 1 }: 创建了一个名为a的变量,并将一个对象{ n: 1 }赋值给它。
*
* 第二步: let b = a: 创建了一个名为b的变量,并将变量a的引用赋值给它,所以b和a引用同一个对象 { n: 1 }。
*
* a.x = a = { n: 2 }:
* 在这儿涉及到运算符的优先级:'.' 的优先级比 '=' 高
*
* 第三步:所以,a.x,此时a、b的结构都为:
* {
* n:1,
* x:undefined
* },a、b还是都引用这一个对象,取名对象A
*
* 第四步,然后,'=' 运算符从右往左,a就变成了{ n:2 }
*
* 此时,栈内存中a、b两个变量,a指向堆内存中的对象B:{ n:2 },b指向堆内存中的对象A: { n:1, x:undefined }
*
* 第五步,接下来,a.x = a,此时,这儿可以理解为对象A的属性x指向了对象B
*
* 当console.log(a.x)的时候,a是指向对象B的,但对象B没有属性x。当查找一个对象的属性时,JavaScript 会向上遍历原型链,
* 直到找到给定名称的属性为止。但当查找到达原型链的顶部 - 也就是 Object.prototype - 仍然没有找到指定的属性B.prototype.x,
* 自然也就输出undefined。
* 而在console.log(b.x)的时候,由于b.x表示对象A的x属性,该属性是指向对象B,自然也输出{ n: 2 }
*
* 所以,最终
* undefined
* { n: 2 }
四、
const numbers = [1, 2, 3]
numbers[10] = 11
console.log(numbers)
*
const numbers = [1, 2, 3]
numbers[10] = 11
console.log(numbers)
/**
* 例题解析:
*
* 在这段代码中,你首先创建了一个名为 numbers 的数组,其中包含了三个元素 [1, 2, 3]。
* 然后,你尝试将数组的第 10 个位置(索引为 10)设置为 11。
* 这会导致数组中间隔了多个未定义的元素,因为索引 3 到 9 之间的元素都会被隐式地设置为 undefined。
* 最终,你将整个数组打印出来。
*/
/**
*
* 在这个输出中,<7 empty items> 表示有 7 个未定义的空元素。这是因为 JavaScript 数组是稀疏的,它们可以包含未定义的元素,而不需要在数组中依次填充每个索引。
*
*/
/**
* 扩展知识点:稀疏数组
*
* 稀疏数组(Sparse Array)是指数组中包含未定义(或者说是空)元素的数组。
*
* 在 JavaScript 中,数组是一个有序的集合,每个元素通过一个索引来访问。正常情况下,数组的索引是连续的,从 0 开始递增,依次排列。
* 但是,JavaScript 中的数组允许存在不连续的索引,即某些位置上没有对应的元素。
*
* 在稀疏数组中,数组的某些索引位置没有存储实际的元素值,而是被标记为“空”。这就导致数组的长度(由 length 属性表示)可能比实际存储的元素数量要大得多。
*
* 稀疏数组可以出现在一些特殊情况下,例如:
*
* 直接设置元素: 如果你在一个数组中设置了一个非连续的索引位置的元素,那么这个索引之前的位置都会被视为稀疏。
* 删除元素: 删除数组中的某个元素,并不会影响数组的长度,导致被删除元素的位置变为空。
* 使用 delete 关键字: 使用 delete 关键字删除数组的某个元素,同样会使数组变得稀疏。
*
* **重点**:稀疏数组在某些情况下可能会导致不太直观的行为,因为使用 for...in 循环或 forEach 等迭代方法时,可能会跳过空的索引位置。
*/
/**
* (1)通过for循环遍历例题中的numbers数组:
*/
// for (i = 0; i < numbers.length; i++) {
// console.log(numbers[i])
// }
/**
* 结果为:1
* 2
* 3
* undefined
* undefined
* undefined
* undefined
* undefined
* undefined
* undefined
* 11
*
*/
/**
* (2)通过forEach遍历
*/
// numbers.forEach((item) => {
// console.log(item)
// })
/**
* 结果为:1
* 2
* 3
* 11
*/
/**
* (3)for...of循环
*/
// for (const item of numbers) {
// console.log(item)
// }
/**
* 结果为:1
* 2
* 3
* undefined
* undefined
* undefined
* undefined
* undefined
* undefined
* undefined
* 11
*
*/
/**
* (4)for...in循环
*/
// for (const key in numbers) {
// console.log(numbers[key])
// }
/**
* 结果为:1
* 2
* 3
* 11
*/
for…in循环用于遍历对象的属性,返回的是属性名。而for…of循环用于遍历可迭代对象(如数组、字符串、Set、Map等),返回的是元素值。
五、
// 4.下面代码的输出是什么?
(() => {
let x, y
try {
throw new Error()
} catch (x) {
;(x = 1), (y = 2)
console.log(x)
}
console.log(x)
console.log(y)
})()
/**
* 自执行匿名函数
*/
/**
* 基础知识:try...catch 语句中的throw语句
*
* 当在 JavaScript 中使用 try...catch 语句时,throw 语句用于手动创建一个异常,然后在 catch 块中捕获和处理这个错误。
*
*
* throw 语句的作用:
* 1.创建异常对象: throw 语句会在执行到它的位置时创建一个异常对象,这个异常对象通常是一个 Error 类型的实例或其子类。
*
* 2.中断代码执行: 一旦 throw 语句被执行,代码的正常执行流程会立即中断。这意味着 throw 语句后面的代码不会被执行。
*
* 3.(**重点**)此时,在 try 块中,如果异常被抛出,控制流会跳到 catch 块中,这里我们可以访问并处理异常。
*
* 4.传递到 catch 块: 抛出的异常会被 JavaScript 运行时引擎捕获,然后尝试匹配适合的 catch 块来处理这个异常。如果在当前作用域内找不到匹配的 catch 块,
* 异常将被传递到调用堆栈上一层的作用域,以此类推,直到找到合适的 catch 块或者直到异常未被捕获。
*
* 总结:throw语句抛出一个异常的实例对象,如果在try中执行到了throw语句,那么try里面的throw语句之后的代码就不会再执行,直接去执行catch里面的代码(看例题就懂了)
*/
// 例题:
// function divide(a, b) {
// if (b === 0) {
// throw new Error('Division by zero is not allowed.')
// }
// return a / b
// }
// try {
// const result = divide(10, 0)
// console.log(111) // 这行不会被执行
// console.log(result) // 这行不会被执行
// } catch (error) {
// console.log('An error occurred:', error.message)
// }
// 在这个例子中,throw 语句被用来检查除数是否为零,如果是零,就抛出一个包含错误信息的 Error 对象。
// 然后,在 try 块中,如果异常被抛出,控制流会跳到 catch 块中,这里可以访问并处理异常。
/**
* 本题解析:
*
* 这段代码中的关键是 catch 块内部的 throw 语句。实际上,这里的 throw new Error() 是为了创建一个错误,但是该错误并没有被捕获。让我们来一步步解释:
*
* 来解释代码的执行:
*
* 第一步:在立即调用的函数表达式内部,声明了两个变量 x 和 y。
*
* 第二步:在 try 块中,抛出一个 Error 对象。由于没有匹配的 catch 块来捕获这个错误,所以它会沿着函数调用堆栈向上抛出,直到被全局错误处理机制捕获(如果有的话)。
*
* 第三步:在 catch 块内部,有一个变量 x,它是一个参数,而不是外部作用域中的变量。在 catch 块内,将参数 x 的值设置为 1,将 y 的值设置为 2。
*
* 第四步:在 catch 块内,执行 console.log(x) 会输出 1,因为它引用的是 catch 块内部的参数 x。
*
* 第五步:在 catch 块之外,输出外部作用域中的变量 x。由于 catch 块的参数 x 不影响外部作用域中的变量 x,所以这里的 console.log(x) 输出的是外部作用域中的 x,
* 但是在外部作用域中,它声明未赋值,因此输出为 undefined。
*
* 第六步:由于 y 在 catch 块内被赋值为 2,并且y具有函数作用域,所以在函数内部的任何地方都可以访问并输出 y,输出为 2。
六、
function sayHi() {
return (() => 0)()
}
console.log(sayHi())
console.log(sayHi)
/**
* 本题解析:
* 1.在sayHi函数内部,return后面是一个自执行匿名函数,调用返回值为0,所以return后面值为0,相当于sayHi函数的返回值为0
* 2.在typeof后面,要注意是sayHi(),而不是sayHi,这表明调用了sayHi函数,而sayHi函数的返回值为0,所以typeof判断的其实是
* sayHi函数返回值的类型,typeof sayHi() 就相当于 typeof 0,因此这儿判断类型为number
*/
/**
* 注意:如果是 let type = typeof sayHi
console.log(type)
那么这儿的输出结果就是`function`
*/
七
const person = { name: 'Lydia' }
function sayHi(age) {
console.log(`${this.name} is ${age}`)
}
sayHi.call(person, 21)
sayHi.bind(person, 21)
* call、apply、bind的区别:
* 相同:(1)都是用来改变函数的this对象的指向的
* (2)第一个参数都是this要指向的对象
* (3)都可以利用后续参数传参
* 不同:(1)执行方式不同:call和apply是改变后页面加载之后立即执行,bind是异步代码,改变之后不会立马执行,而是返回一个新的参数
* (2)传参方式不同:call和bind是一个一个传入,apply可以使用数组的方式传入
* (3)修改this的性质不同:call、apply只是临时修改一次,bind是永久修改this指向
*/
/**
* this指向问题:
* (1)普通函数调用:通过函数名()直接调用,this指向全局对象window
* (2)构造函数调用:用new关键字调用时,this指向新new出的对象
* (3)对象函数调用:object.method(),this指向这个对象
* (4)call、apply、bind:method.call(ctx),this指向第一个参数
* (5)箭头函数调用:箭头函数里没有this,所以永远是上层作用域this
*/
/**
* 在 JavaScript 中,this 在 Node.js 环境和浏览器环境中的行为有一些不同之处。
*
* 1.浏览器环境:
* 在浏览器环境中,this 的行为可能会根据执行上下文的不同情况而变化:
*
* 全局上下下文中: 在浏览器环境下的全局作用域中,this 指向 window 对象,即全局对象。
*
* 函数上下文中: 在函数内部,this 的值取决于函数是如何被调用的。如果函数是作为普通函数调用,那么 this 将指向全局对象(在非严格模式下),
* 或者是 undefined(在严格模式下)。如果函数是作为对象的方法调用,this 将指向调用该方法的对象。
*
* 2.Node.js 环境:
* 在 Node.js 环境中,this 的行为也会受到上下文影响,但有一些区别:
*
* 全局上下文中: 在 Node.js 的全局上下文中,this 并不指向 window 对象,而是指向一个特殊的对象,称为 global 对象,它类似于浏览器环境中的 window 对象。
*
* 模块上下文中: 在 Node.js 中,每个模块都有自己的上下文,而不像浏览器中的全局上下文。在模块内部,this 不会指向 global,而是指向模块本身的导出对象。
*
* 需要注意的是,使用箭头函数可以改变 this 的行为,使其与普通函数有所不同。在箭头函数中,this 的值由外围(包含箭头函数的函数或上下文)的 this 值决定,
* 而不是函数自身的调用方式。
*
* 总之,在不同的上下文中,this 的值可能会有所不同,因此在编写代码时需要注意上下文的影响。
*/
/**
* 本题解析
*/
/**
*
* 这段代码涉及了 JavaScript 中的 call 和 bind 方法,以及 this 关键字的用法。让我们逐步解释:
*
* 首先,创建了一个名为 person 的对象,其中包含一个 name 属性。
*
* 然后,定义了一个名为 sayHi 的函数,它接受一个 age 参数。在函数内部,使用 this.name 来引用调用该函数的对象的 name 属性。
*
* 使用 call 方法调用 sayHi 函数,并将 person 对象作为第一个参数传递给 call 方法。第二个参数是传递给 sayHi 函数的参数,即 21。
* 这样,sayHi.call(person, 21) 会输出 Lydia is 21。这是因为 this 在 sayHi 函数内指向了 person 对象。
*
* 使用 bind 方法也是类似的,但它不会立即调用函数,而是创建一个新的函数,将指定的 this 值和参数预设。
* 然而,在你的代码中,你并没有调用绑定后的函数,因此 sayHi.bind(person, 21) 并没有产生输出。
*
* 综合来说,只有 sayHi.call(person, 21) 被调用并产生了输出,而 sayHi.bind(person, 21) 只是返回了一个新的函数,
* 需要再次调用它才能产生输出。可以这样写:
*
* const boundFunction = sayHi.bind(person, 21);
* boundFunction(); // 输出 "Lydia is 21"
*/
八
/**
* Tips:先把前置知识看完,再看本题解析
*/
// 3.下面代码的输出是什么?
var arr = [
[0, 1],
[2, 3]
]
var result = arr.reduce(
(acc, cur) => {
return acc.concat(cur)
},
[1, 2]
)
console.log(result)
// [ 1, 2, 0, 1, 2, 3 ]
/**
* 解析:
* 本题为数组扁平化
* 第一步:acc为传入的[1,2],当前项为[0,1],执行acc.concat(cur),则返回值为[ 1 , 2 , 0 , 1 ]
* 第二部:acc为上次返回值[ 1 , 2 , 0 , 1 ],再和第二项[ 2 , 3 ]合并,最终为[ 1, 2, 0, 1, 2, 3 ]
*/
/*----------------------------前置基础知识----------------------------*/
/**
* 一、reduce()函数
* 定义
* 1.reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。
*
* 2.reduce() 可以作为一个高阶函数,用于函数的 compose
*
* 3.reduce() 对于空数组是不会执行回调函数的
*
* 4.reduce() 会遍历数组的每一项
*
*/
/**
* 语法
* array.reduce(function(prev, currentValue, currentIndex, arr), initialValue)
*
* 说明:
* reduce函数,接收两个参数,一个为回调函数,一个为初始赋值,最常用的场景就是,计算数组中的每一项的总和。
*
* 第一个参数的回调函数又接收四个参数,分别为(初始值或计算结束后的返回值,当前元素,当前元素的索引,当前元素所属的数组对象)
*
* 第二个参数是传给函数的初始值,非必传
*
* prev:函数传进来的初始值或上一次回调的返回值
* currentValue:数组中当前处理的元素值
* currentIndex:当前元素索引
* arr:当前元素所属的数组本身
* initialValue:传给函数的初始值
*/
// 例题1
/**
var arr = [1, 5, 8, 6, 4]
var sum = arr.reduce((prev, cur, index, arr) => {
console.log(prev, cur, index)
return prev + cur
}, 2)
console.log(sum)
*/
/**
* 例题1解析:
* 首先明确一点:reduce() 方法接收一个函数作为累加器,理解累加器的意思
* 第一步:prev是传进来的值`initialValue`为2,注意:这儿如果没有传值,则为第一个值1,所以prev, cur, index分别是2,1,0
* 第二步:prev:函数上一次回调的返回值,也就是return 后面prev+cur的值,就是2+1,即prev为3,prev, cur, index分别是3,5,1
* 第三步:prev:prev+cur的值,就是3+5,即prev为8,prev, cur, index分别是8,8,2
* 第四步:prev:16,prev, cur, index分别是16,6,3
* 第五步:prev:22,prev, cur, index分别是22,4,4
* 最终:sum的值为reduce函数返回值,即最后一次执行的return值,也就是26
*/
// 总结:如果没有提供initialValue,reduce 会从索引1的地方开始执行回调方法,跳过第一个索引。如果提供initialValue,从索引0开始。
/**
* prev cur index
* 2 1 0
* 3 5 1
* 8 8 2
* 16 6 3
* 22 4 4
*/
/**
* 二、concat()方法
*
* concat()方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。字符串同理
*
* 语法:Array.concat(val1[,val2[,val3...[,valN]]]) 或者 arr1.concat(arr2)
*
* 参数:valN:可以填数组或值,把他们合并到一个新数组里,原数组不变,如果不写,则默认对Array进行一个浅复制
*/
九、
// 2.结果是什么?
let result = [...'Lydia']
console.log(result)
/**
* 扩展运算符:对象中的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中
*/
/**
* 在ES6中。 三个点(...) 有2个含义。分别表示 扩展运算符 和 剩余运算符。
*/
/******************扩展运算符(spread)********************/
// 扩展运算符的主要作用是将一个数组转为用逗号分隔的参数序列,它好比 rest 的逆运算
// 最后总结:
/**
* 扩展运算符用三个点号表示,功能是把数组或类数组对象展开成一系列用逗号隔开的值
* rest运算符也是三个点号,不过其功能与扩展运算符恰好相反,把逗号隔开的值序列组合成一个数组
*
* 当三个点(...)在等号左边,或者放在形参上。为 rest 运算符
* 当三个在等号右边,或者放在实参上,是 spread运算符
*
* 或者说:放在被赋值一方是rest 运算符。放在赋值一方是 spread运算符。
*/
十、
var a = 0,
b = 0
function A(a) {
A = function (b) {
console.log(a + b++)
}
console.log(a++) // 第一次执行到这的时候,变量a在内存中的值已经为2
}
A(1)
A(2)
// 1.首先,全局变量'a'和'b'被初始化为0
// 2.调用函数A(1),进入函数体内:
/**
* (1)传入参数 a 为1,
* (2)函数A被重新赋值为一个新的函数
* (3)在函数内部,打印输出a++,此时打印出的a为1,然后进行a++,在内存中a的值为2
*/
// 3.调用函数A(2),进入函数体内:
/**
* (1)A函数已被定义,此时执行的是新定义的函数 function(b)
* (2)传入b的值为2
* (3)在新定义的函数内部,计算并输出a+b++,其中a为之前的参数2,b是2(传入的参数)
* (4)打印出a+b++的结果为4,b再执行自增,最终b的值变成3
*/
/**
* (1)变量作用域和提升: JavaScript 中的变量有不同的作用域,包括函数作用域和全局作用域。
* 在函数内部重新定义变量会覆盖外部的同名变量。此外,变量和函数在执行之前会被提升到作用域的顶部。
*
* (2)函数声明和函数表达式: JavaScript 中可以使用函数声明和函数表达式来创建函数。函数声明会被提升,
* 而函数表达式则不会。在这个题目中,函数 A 被反复赋值,所以会导致不同的行为。
*
* (3)闭包: 在 JavaScript 中,函数可以访问其外部作用域中的变量。在这个题目中,函数 A 的内部函数(也叫闭包)
* 会保留对外部变量 a 的引用,即使 A 函数被重新赋值,闭包中的引用仍然存在。
*
* (4)参数传递和自增运算: 函数可以接受参数,并在内部进行操作。自增运算符 ++ 会增加变量的值。
*
* (5)函数调用和执行顺序: 函数按照调用的顺序依次执行,但由于 JavaScript 的事件循环机制,异步操作可能会导致某些代码在其他代码之前执行。
*/
相关作者
- 获取点赞0
- 文章阅读量264
评论()