目录
let、const、var
let、const、var 的区别
1.是否存在变量提升?
var
声明的变量存在变量提升(将变量提升到当前作用域的顶部)。即变量可以在声明之前调用,值为undefined
let
和const
不存在变量提升。即它们所声明的变量一定要在声明后使用,否则报ReferenceError
错
2.是否存在暂时性死区?
let和const存在暂时性死区
。即只要块级作用域内存在 let 命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响
3.是否允许重复声明变量?
var
允许重复声明变量。let
和const
在同一作用域不允许重复声明变量。
4.是否存在块级作用域?
- var 不存在块级作用域。
- let 和 const 存在块级作用域
- 块作用域由
{ }
包括,if
语句和for
语句里面的{ }
也属于块作用域
- 块作用域由
5. 是否能修改声明的变量?
var
和let
可以。const
声明一个只读的常量。一旦声明,常量的值就不能改变。const
声明的变量不得改变值,这意味着,const
一旦声明变量,就必须立即初始化,不能留到以后赋值。
什么是暂时性死区(TDZ)
在代码块内,使用
let、const
命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
if (true) {
// TDZ开始
a = 'abc'; // ReferenceError
console.log(a); // ReferenceError
let a; // TDZ结束
console.log(a); // undefined
a = 123;
console.log(a); // 123
}
解构赋值
- 数组解构
const foo = ["one", "two", "three"];
const [red, yellow, green] = foo;
console.log(red); // "one"
console.log(yellow); // "two"
console.log(green); // "three"
- 对象解构
const obj = {
a: "1",
b: "2",
}
const { a, b } = obj
console.log(a, b) // 1 2
- 默认参数
const obj = {
a: "1",
}
const { a, b = 'b' } = obj
console.log(a, b) // 1 b
- 剩余参数
...
const foo = ["one", "two", "three"];
const [a, ...b] = foo;
console.log(a); // "one"
console.log(b); // [ 'two', 'three' ]
- 赋值给新的变量名
const o = { p: 42, q: true };
const { p: foo, q: bar } = o;
console.log(foo); // 42
console.log(bar); // true
数组遍历
for
forEach
map
- 返回新数组,不会改变原数组
filter
- 过滤some
- 返回布尔值,有一个符合条件就返回 true
every
- 返回布尔值,每一个都符合条件才返回 true
reduce
for...of
、for...in
- [[for…in和for…of]]
find
- findIndex
- values ()
- keys ()
- entries ()
const arr = [1, 2, 3]
for (const val of arr.values()) {
console.log(val) // 1 2 3
}
for (const val of arr.keys()) {
console.log(val) // 0 1 2
}
for (const [index, item] of arr.entries()) {
console.log(index, item)
// 0 1
// 1 2
// 2 3
}
数组的扩展
Array.from ()
从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。
const set = new Set(['foo', 'bar', 'baz', 'foo']);
Array.from(set);
// [ "foo", "bar", "baz" ]Ï
Array.of()
创建一个具有可变数量参数的新数组实例,而不考虑参数的数量或类型。
const arr = Array.of(1, true, "123", [1, 2, 3], { age: 18 })
console.log("🚀 ~ ", arr) // [ 1, true, '123', [ 1, 2, 3 ], { age: 18 } ]
fill()
用一个固定值填充一个数组中从起始索引(默认为 0)到终止索引(默认为 array.length)内的全部元素。它返回修改后的数组。
[1, 2, 3].fill(4); // [4, 4, 4]
[1, 2, 3].fill(4, 1); // [1, 4, 4]
[1, 2, 3].fill(4, 1, 2); // [1, 4, 3]
[1, 2, 3].fill(4, 1, 1); // [1, 2, 3]
[1, 2, 3].fill(4, 3, 3); // [1, 2, 3]
[1, 2, 3].fill(4, -3, -2); // [4, 2, 3]
[1, 2, 3].fill(4, NaN, NaN); // [1, 2, 3]
[1, 2, 3].fill(4, 3, 5); // [1, 2, 3]
Array(3).fill(4); // [4, 4, 4]
[].fill.call({ length: 3 }, 4); // {0: 4, 1: 4, 2: 4, length: 3}
// Objects by reference.
var arr = Array(3).fill({}) // [{}, {}, {}];
arr[0].hi = "hi"; // [{ hi: "hi" }, { hi: "hi" }, { hi: "hi" }]
includes()
用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false。
[1, 2, 3].includes(2); // true
[1, 2, 3].includes(4); // false
[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true
[1, 2, NaN].includes(NaN); // true
[1, 2, NaN].indexOf(NaN) // -1
函数的参数
参数的默认值
const fn = (a, b) => {
console.log(a, b) // 1 2
}
fn(1, 2)
const fn = (a, b = 2) => {
console.log(a, b) // 1 2
}
fn(1)
const fn = (a = 1, b) => {
console.log(a, b) // 10 undefined
}
fn(10)
注意:默认参数应该写在后面,必填参数放在前面
解构赋值
[[#解构赋值]]
const fn = (name, { age = 18, work = "front" } = {}) => {
console.log(name, age, work)
}
fn("zs") // zs 18 front
fn("zs", { age: 20 }) // zs 20 front
length 属性
const fn = (a, b, c) => {}
console.log(fn.length) // 3
const fn2 = (a, b, c = 1) => {}
console.log(fn2.length) // 2
可以使用 length 获取对象必传参数的个数
函数参数作用域
const x = 13;
function fn(x, y = x) {
console.log(y);
}
fn(1); // 输出 1
在这个例子中,我们定义了一个函数 fn
,它接受两个参数 x
和 y
,其中 y
的默认值为 x
。当我们调用 fn(1)
时,函数内部的 x
被赋值为1,而由于没有传入 y
的值,所以 y
的默认值为函数内部的 x
,因此 y
的值也为1,所以最终打印出来的是1
const x = 13;
function fn2(y = x) {
console.log(y);
}
fn2(); // 输出 13
在这个例子中,我们定义了一个函数 fn2
,它只接受一个参数 y
,且 y
的默认值为外部定义的 x
。当我们调用 fn2()
时,由于没有传入任何参数,所以 y
的默认值为外部的 x
,即13,因此最终打印出来的是13。
function fn3(y = x) {
const x = 2;
console.log(y);
}
fn3(); // 输出:x is not defined
在这个例子中,我们定义了一个函数fn3
,它只接受一个参数y
,且y
的默认值为外部定义的x
。但是,在函数内部,我们又定义了一个局部变量x
并赋值为2。当我们调用fn3()
时,JavaScript会在函数体内查找默认参数y
的值,但此时x
还未定义,因此会报错x is not defined
。
这些例子展示了JavaScript函数参数作用域的一些特点:
- 函数内部的参数会覆盖外部定义的同名变量。
- 默认参数的作用域是在函数体内部。
- 函数内部定义的局部变量会遮蔽外部同名变量,但默认参数的作用域不受影响。
扩展运算符、剩余参数、arguments
详情见文档:
箭头函数
() => {}
箭头函数与普通函数区别
- 箭头函数是匿名函数,不能作为构造函数,不能使用 new
- 箭头函数内没有
arguments
,可以用扩展运算符...
解决 - 箭头函数没有
this
, 箭头函数的this
,始终指向父级上下文(箭头函数的this
取决于定义位置父级的上下文
,跟使用位置没关系,普通函数this
指向调用的那个对象) - 箭头函数不能通过
call() 、 apply() 、bind()
方法直接修改它的 this 指向。(call、apply、bind
会默认忽略第一个参数,但是可以正常传参) - 箭头函数没有原型属性
对象的扩展
属性简写
const name = "zs"
const age = 18
const person = {
name,
age,
}
console.log(person) // { name: 'zs', age: 18 }
属性名表达式
const name = "zs"
const age = 18
const s = 'school'
const person = {
name,
age,
[s]: 'xx',
study() {
console.log(this.name + '爱学习')
}
}
console.log(person) // { name: 'zs', age: 18, school: 'xx', study: [Function: study] }
person.study() // zs爱学习
Object.is ()
Object.is()静态方法确定两个值是否为相同值。
// 案例 1:评估结果和使用 === 相同
Object.is(25, 25); // true
Object.is("foo", "foo"); // true
Object.is("foo", "bar"); // false
Object.is(null, null); // true
Object.is(undefined, undefined); // true
Object.is(window, window); // true
Object.is([], []); // false
const foo = { a: 1 };
const bar = { a: 1 };
const sameFoo = foo;
Object.is(foo, foo); // true
Object.is(foo, bar); // false
Object.is(foo, sameFoo); // true
// 案例 2: 带符号的 0
Object.is(0, -0); // false
Object.is(+0, -0); // false
Object.is(-0, -0); // true
// 案例 3: NaN
Object.is(NaN, 0 / 0); // true
Object.is(NaN, Number.NaN); // true
Object.is() 与 == 运算符并不等价。== 运算符在测试相等性之前,会对两个操作数进行类型转换(如果它们不是相同的类型),这可能会导致一些非预期的行为,例如 “” == false 的结果是 true,但是 Object.is() 不会对其操作数进行类型转换。
Object.is() 也不等价于 === 运算符。Object.is() 和 === 之间的唯一区别在于它们处理带符号的 0 和 NaN 值的时候。=== 运算符(和 == 运算符)将数值 -0 和 +0 视为相等,但是会将 NaN 视为彼此不相等。
Object.assign()
Object.assign() 用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
const obj = { a: 1 };
const copy = Object.assign({}, obj);
console.log(copy); // { a: 1 }
in
in :如果指定的属性在指定的对象或其原型链中,则 in
运算符**返回 true
。
// 数组
var trees = new Array("redwood", "bay", "cedar", "oak", "maple");
0 in trees // 返回true
3 in trees // 返回true
6 in trees // 返回false
"bay" in trees // 返回false (必须使用索引号,而不是数组元素的值)
"length" in trees // 返回true (length是一个数组属性)
Symbol.iterator in trees // 返回true (数组可迭代,只在ES2015+上有效)
// 内置对象
"PI" in Math // 返回true
// 自定义对象
var mycar = {make: "Honda", model: "Accord", year: 1998};
"make" in mycar // 返回true
"model" in mycar // 返回true
遍历对象
for... in
Keys ()
Object.getOwnPropertyNames ()
Reflect.ownKeys ()
深拷贝、浅拷贝
基本类型数据保存在在栈内存中
引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中
深拷贝和浅拷贝的区别
1.浅拷贝: 将原对象或原数组的引用直接赋给新对象,新数组,新对象/数组只是原对象的一个引用。
如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址,即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址
2.深拷贝: 创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”
浅拷贝
数组浅拷贝:
// 直接遍历
function shallowCopy(arr) {
const newArr = [];
arr.forEach((item) => newArr.push(item));
return newArr;
}
// slice
const arr2 = [1, 2, 3, 4, 5];
const newArr2 = arr2.slice();
// concat() 合并空数组实现
const arr3 = [11, 22, 33, 44, 55];
const newArr3 = arr3.concat([]);
对象浅拷贝:
// 直接遍历
function shallowCopy(obj) {
const newObj = {};
for (let item in obj) {
newObj[item] = obj[item];
}
return newObj;
}
// 使用拓展运算符
const obj2 = { name: "Bob", age: 17 };
const newObj2 = { ...obj2 };
深拷贝
用深拷贝最后要递归到全部是基本值,不然可能会陷入死循环/循环引用,导致栈溢出
TODO(待理解)⭐: 处理过的数据使用 map 结构缓存起来 >> 递归的时候碰到相同的数据 >> 直接使用缓存里面的
**1. 先转换成字符串,在转换成(数组/对象) JSON.parse(JSON.stringify(XXXX))
[!tip] 注意
缺点:不能拷贝里面的函数
const array = [{ number: 1 }, { number: 2 }, { number: 3 }];
const str = JSON.stringify(array);
const copyArray = JSON.parse(str);
2、递归实现简单的深拷贝:
const cloneDeep = (target) => {
const getType = (data) => {
return Object.prototype.toString.call(data).slice(8, -1)
}
const targetType = getType(target)
if (targetType === "Object") {
const result = {}
Object.getOwnPropertyNames(target).forEach(key => {
result[key] = cloneDeep(target[key])
})
return result
} else if (targetType === "Array") {
return target.map(item => deepClone(item))
} else {
return target
}
}
3、解构赋值法实现一层拷贝
4、Object.assign() 实现一层深拷贝
5、 lodash第三方库
在引用类型的情况下,拷贝分为浅拷贝和深拷贝:
- 浅拷贝复制的是对象的引用,因此当属性为对象时,浅拷贝只复制一层,导致两个对象指向同一个地址。
- 深拷贝则是递归地复制对象的所有层次,包括属性为对象的情况,因此深拷贝会创建一个全新的对象栈,使得两个对象指向不同的地址。