我正在参加「掘金·启航计划」
let 和 const
let
用来声明变量,只在let命令所在的代码块内有效,即块级作用域。不存在变量提升,不允许重复声明
function varTest() {
var a = 1
if (true) {
var a = 2
console.log(a) // 2
}
console.log(a) // 2
}
function letTest() {
console.log(b) // ReferenceError: b is not defined
let b = 1
// let b = 2; // Uncaught SyntaxError: Identifier 'b' has already been declared
if (true) {
let b = 2
console.log(b) // 2
}
console.log(b) // 1
}
在letTest()的 if 语句中,可以再次声明变量 b,是因为变量 b 只在这个 if 语句中有效。
如果在 if 语句中使用var声明变量 b,会报错
let 很适合在 for 循环时声明索引变量
const
const声明一个只读的常量,必须初始化赋值。一旦声明,常量的值就不能改变,只在声明所在的块级作用域内有效。
不存在变量提升,不允许重复声明。复杂类型(数组、对象等)指针指向的地址不能更改,内部数据可以更改
const a = '123'
a = '234' // TypeError: Assignment to constant letiable
const arr = [1, 2, 3]
arr.push(4)
console.log(arr) // [1,2,3,4]
arr = []
console.log(arr) // 改变数组的指向会出错 Uncaught TypeError: Assignment to constant letiable
let 和 const 声明的全局变量不属于顶层对象的属性,只存在于块级作用域中
let a = 1
const b = 2
console.log(window.a) // undefined
console.log(window.b) // undefined
模板字符串
模板字符串(templatestring)是增强版的字符串,用反引号`标识,嵌入的变量名写在${}之中。
第一个用途,基本的字符串格式化。
const name = 'world'
// ES5
console.log('hello' + name)
// ES6
console.log(`hello${name}`)
第二个用途,做多行字符串或者字符串一行行拼接。
// ES5
let a =
'Hi \
Girl!'
// ES6
let say = `<div>
<p>hello, world</p>
</div>`
ES6 还提供了一些字符串方法,如下:
// 1.includes:判断是否包含参数字符串,返回布尔值
const str = 'welcome'
console.log(str.includes('e')) // true
// 2.repeat: 获取字符串重复n次
const str = 'he'
console.log(str.repeat(3)) // 'hehehe'
// 如果带入小数, Math.floor(num) 来处理
// s.repeat(3.1) 或者 s.repeat(3.9) 都当做 s.repeat(3) 来处理
// 3. startsWith 和 endsWith 判断是否以给定文本开始或者结束
const str = 'hello world!'
console.log(str.startsWith('hello')) // true
console.log(str.endsWith('!')) // true
解构赋值
数组的解构赋值
可以从数组中提取值,按照对应位置,对变量赋值。这种写法属模式匹配,只要等号两边的模式相同,左边的变量就会被赋予对应的值
let [a, b, c] = [1, 2, 3]
console.log(a, b, c) // 1 2 3
注意细节:
1、左右结构不同
let [a, b, c, d] = [1, 2, 3]
console.log(a, b, c, d) // 1 2 3 undefined
2、跳过部分
let [a, , c] = [1, 2, 3]
console.log(a, c) // 1 3
3、默认值
let [a, b, c, d = 666] = [1, 2, 3]
console.log(a, b, c, d) // 1 2 3 666
let [a = 11, b = 22, c, d = 666] = []
console.log(a, b, c, d) // 11 22 undefined 666
4、嵌套
let [a, b, c] = [1, 2, [3]]
console.log(a, b, c) // 1 2 [3]
let [a, b, [c]] = [1, 2, [3]]
console.log(a, b, c) // 1 2 3
对象的解构赋值
let { name, age } = { name: 'zgh', age: 22 }
console.log(name, age) // zgh 22
对象与数组解构的不同点
- 数组的元素是按次序排列的,变量的取值由它的位置决定
- 对象的属性没有次序,变量必须与属性同名,才能取到正确的值
函数参数的解构赋值
let f = ([a, b]) => a + b
f([1, 2]) // 3
上述代码可将数组[1, 2]看作一个参数param,即param = [1, 2]
函数
为函数的参数设置默认值
可以给函数的参数设置默认值,如果不指定该函数的参数值,就会使用默认参数值
function Person(name = 'zgh', num = 22) {
const name = name || 'zgh'
const num = num || 22
}
Person()
Person('Jack', 20)
如果没有设置默认值,调用时 num 传入 0,0 为 false,那么例子中的 num 结果就为 22 而不是 0
箭头函数
ES6 允许使用“箭头”(=>)定义函数
//1.不带参数
let sum = () => 1 + 2
//等同于
let sum = function () {
return 1 + 2
}
//2.带一个参数
let sum = (a) => a
//等同于
let sum = function (a) {
return a
}
//3.带多个参数,需要使用小括号将参数括起来
let sum = (a, b) => a + b
//等同于
let sum = function (a, b) {
return a + b
}
//4.代码块部分多于一条语句需要用大括号将其括起来,并且使用return语句返回。
let sum = (a, b) => {
let c = a + b
return c
}
//5.返回对象,就必须用小括号把该对象括起来
let person = (name) => ({ name: 'zgh', age: 22 })
//等同于
let person = function (name) {
return { name: 'zgh', age: 22 }
}
箭头函数的 this 指向
箭头函数本身是没有this和arguments的,在箭头函数中引用 this 实际上是调用的是定义时的父执行上下文的 this。简单对象(非函数)是没有执行上下文的。
- 使用
call,apply,bind都不能改变 this 指向 - 箭头函数没有原型属性
prototype - 不能用作构造函数,即 new 指令
let obj = {
say() {
let f1 = () => console.log(this)
f1()
}
}
let rs = obj.say
rs() // f1执行时,say函数指向window,所以f1中的this指向window
obj.say() // f1执行时,say函数指向obj,所以f1中的this指向obj
// 下面写法错误!
let Person = (name) => {
this.name = name
}
对象
对象简写
- 属性的简写
条件:属性的值是一个变量,且变量名称和键名是一致的
let name = 'zgh'
let age = 22
// ES5写法
let obj = { name: name, age: age }
// ES6写法
let obj = { name, age }
- 方法的简写
// ES5写法
let obj = {
hello: function () {
console.log('hello')
}
}
// ES6写法
let obj = {
hello() {
console.log('hello')
}
}
Map
类似于对象,是键值对的结构,区别在于:对象中的键名只能是字符串或者Symbol,而 Map 里面的键可以是任意值。
//创建一个Map对象
let myMap = new Map()
//添加键值对
myMap.set('a', 'hello')
myMap.set([1, 2, 3], { name: 'zgh' })
// 也可以在声明时就添加键值对,二维数组
const user = new Map([
['foo', 'zgh'],
['baz', 23]
])
//查看集合中元素的数量
myMap.size
//获取相应的键值
myMap.get('a')
//删除一个键值对,然后再判断该键值对是否存在
myMap.delete('a')
myMap.has('a')
//删除Map集合中所有的键值对
myMap.clear()
Set
Set对象是一组不重复的、无序的值的集合,可以往里面添加、删除、查询数据。
Set本身是一个构造函数,用来生成Set数据结构
Set()接受具有iterable可迭代接口的数据结构作为参数,如数组、类数组、字符串等。
不能接受对象结构,否则报错Uncaught TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))
// 声明一个Set对象
let mySet = new Set()
let mySet2 = new Set([1, 2, 3])
// 添加元素
mySet.add(1)
mySet.add('hi')
mySet.add([2, 'hello'])
// 判断集合中是否存在一个元素1
mySet.has(1) // true
// 删除集合中的字符串
mySet.delete('hi')
// 获取集合中元素的数量
mySet.size // 3
// 遍历
mySet.forEach((item) => console.log(item))
// 删除集合中所有的元素
mySet.clear()
// 两个对象是不相等的
const set2 = new Set()
set2.add({})
set2.size // 1
set2.add({})
set2.size // 2
遍历操作
let mySet = new Set(['a', 'b', 'c'])
// entries()返回的遍历器同时包括键名和键值,二者一样
for (let i of mySet.entries()) {
console.log(i)
}
// ["a", "a"]
// ["b", "b"]
// ["c", "c"]
// keys()返回键名
for (let i of mySet.keys()) {
console.log(i)
}
// 'a'
// 'b'
// 'c'
// values()返回键值,结果同keys()
for (let i of mySet.values()) {
console.log(i)
}
Set只存储唯一值,可用来数组去重
let arr = [1, 1, 2, 2, 3, 3]
let res1 = [...new Set(arr)] // [1, 2, 3]
// 或者使用 Array.from()
let res2 = Array.from(new Set(arr)) // [1, 2, 3]
也可以用来字符串去重
const str = [...new Set('ababbc')].join('')
console.log(str) // 'abc'
...操作符
...可以叫做 spread(扩展)或者 rest(剩余)操作符
剩余运算符一般会用在函数的参数里面。比如想让一个函数支持更多的参数,参数的数量不受限制,这个时候就可以使用剩余操作符
function Name(x, y, ...z) {
console.log(x) // a
console.log(y) // b
console.log(z) //["c" "d" "e"]
}
Name('a', 'b', 'c', 'd', 'e')
剩余操作符后面的变量会变成一个数组,多余的参数会被放入这个数组中
扩展运算符用在数组的前面,作用就是将这个数组展开
const arr1 = ['a', 'b', 'c', 'd', 'e']
const arr2 = ['f', 'g']
const arr3 = [...arr1, ...arr2] // ["a", "b", "c", "d", "e", "f", "g"]
// 等同于concat
const arr4 = arr1.concat(arr2)
const obj1 = { a: 1, b: 2 }
const obj2 = { ...obj1, c: 3 } // {a: 1, b: 2, c: 3}
使用扩展运算符展开一个新的对象,第二个对象的属性值会覆盖第一个对象的同名属性值
const obj1 = { a: 1, b: 2, c: 3 }
const obj2 = { b: 30, c: 40, d: 50 }
const merged = { ...obj1, ...obj2 }
console.log(merged) // {a: 1, b: 30, c: 40, d: 50}
Class
通过 class 关键字,可以定义类。class 可以看作只是一个语法糖,
它的绝大部分功能,ES5 都可以做到,新的 class 写法让对象原型的写法更加清晰、更像面向对象编程的语法
//ES5 中使用面向对象
function Person(name, age) {
this.name = name
this.age = age
this.say = function () {
console.log('hello')
}
}
let obj = new Person('zgh', 22)
obj.say()
//ES6 中使用面向对象
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
say() {
console.log('hello')
}
}
let obj = new Person('zgh', 22)
obj.say()
上面代码定义了一个类,里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。即 ES5 的构造函数 Person,
对应 ES6 的 Person 类的构造方法。
Person 类除了构造方法,还定义了一个say方法。注意,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。
另外,方法之间不需要逗号分隔,否则会报错。
function Foo() {}
Foo.prototype.constructor === Foo // true
const fo = new Foo()
fo.constructor === Foo // true
Foo.prototype默认有一个公有且不可枚举的construetor属性,这个属性引用的是对象关联的函数(上例中是 Foo)。
构造函数调用new Foo()创建的对象在__proto__上也有construetor属性,指向创建这个对象的函数
Class 继承
class NBAPlayer {
constructor(name, age, height) {
this.name = name
this.age = age
this.height = height
}
say() {
console.log(`我是${this.name},${this.age}岁,身高${this.height}cm`)
}
}
class MVP extends NBAPlayer {
constructor(name, age, height, year) {
super(name, age, height)
this.year = year
}
showMVP() {
console.log(`我是${this.name},在${this.year}获得MVP!`)
}
}
let r1 = new NBAPlayer('Jack', '39', '198')
r1.say()
let r2 = new MVP('Jack', '39', '198', '2010')
r2.showMVP()
extends关键字用于实现类之间的继承。子类继承父类的所有属性和方法,使用super可以调用父类的方法。
静态方法、静态属性
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前加上static关键字,则这个方法不会被实例继承,
而是直接通过类来调用,这就是静态方法。
class Foo {
static f() {
return '666'
}
}
Foo.f() // '666'
let person = new Foo()
person.f() // TypeError: person.f is not a function
父类的静态方法可以被子类继承
class Foo {
static f() {
return '666'
}
}
class Bar extends Foo {}
Bar.f() // "666"
静态方法也可以被super对象调用
class Foo {
static f() {
return '666'
}
}
class Bar extends Foo {
static g() {
return super.f()
}
}
Bar.g() // "666"
类的静态属性
// es6写法
class Foo {}
Foo.prop = 1
// es7写法,推荐这一种写法
class Bar {
static prop = 1
constructor() {
console.log(Bar.prop)
}
}
类的实例属性
类的实例属性可以用等式,写入类的定义之中
class Foo {
state = { value: 1 }
constructor() {
console.log(this.state.value) // 1
}
}
再看看 react 类组件写法,以前定义类的实例属性只能在constructor里面,现在可以写在外面
class Foo extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
}
modalRef = null
}
Promise
promise用同步编程的方式来编写异步代码,解决回调嵌套问题
new Promise((resolve, reject) => {})
Promise 的三种状态
resolved成功rejected失败pending创建 promise 对象实例进行中
then 方法
分别指定resolved状态和rejected状态的回调函数,第二个参数可选(不推荐使用)。
返回的是一个新的Promise,支持链式调用
function pro(params) {
return new Promise((resolve, reject) => {
if (params) {
resolve('hahaha')
} else {
reject('error')
}
})
}
pro(true).then(
(res) => {
console.log(res)
},
(err) => console.log(err)
)
Promise 本身是同步的,then 方法是异步的
const p = new Promise((resolve, reject) => {
console.log(1)
resolve(3)
})
p.then((res) => console.log(res))
console.log(2)
结果是 1、2、3
catch 方法
function Cat(ready) {
return new Promise((resolve, reject) => {
if (ready) {
resolve('Tom')
} else {
reject('Kitty')
}
})
}
Cat(false)
.then((res) => {
console.log(res)
})
.catch((err) => console.log(err))
catch方法可以捕获错误,作用和 then(onFulfilled, onRejected) 当中的 onRejected 函数类似。
Cat(false)
.then((res) => {
console.log(tom)
})
.catch((err) => console.log(err))
示例未定义变量 tom,如果不使用 catch 会直接报错,终止程序。使用后不会报错,但会将错误信息传递到 catch 方法中,方便处理
将catch语句和try/catch语句进行比较?
示例:以下代码输出什么?
try {
;(async function () {
a().b().c()
})()
} catch (e) {
console.log(`执行出错:${e.message}`)
}
答案:Uncaught (in promise) ReferenceError: a is not defined
async定义了异步任务,而try catch无法捕获异步任务,所以无法执行catch语句,
改为同步即可await (async function() { a().b().c() })()
all 和 race 方法
Promise.all()提供并行执行异步操作的能力,将多个实例包装成一个新实例,返回全部实例状态变更后的结果数组(全部变更再返回)
Promise.race()将多个实例包装成一个新实例,返回全部实例状态优先变更后的结果(先变更先返回)
let p1 = new Promise(function (resolve) {
setTimeout(function () {
resolve('Hello')
}, 3000)
})
let p2 = new Promise(function (resolve) {
setTimeout(function () {
resolve('world')
}, 1000)
})
Promise.all([p1, p2]).then((res) => {
console.log(res)
})
Promise.race([p1, p2]).then((res) => {
console.log(res)
})
结果是 1 秒后打印出world,3 秒后打印出["Hello", "world"],表明Promise.all 方法会按照参数数组里面的顺序将结果返回。
Promise.race方法则是只要该数组中的Promise对象的状态发生变化(无论是resolve还是reject)该方法都会返回。
async、await
async、await用来处理异步问题
async放置在函数的前面,返回一个promise
await 只能在 async 函数里面使用,可以让 js 进行等待,直到一个 promise 执行并返回它的结果,js 才会继续往下执行
async function f() {
let res = await axios.get(url)
return res.data // 等待返回请求结果后才执行
}
f()
generator
Generator 是一种异步编程解决方案,执行 Generator 函数会返回一个遍历器对象。两个特征:星号*、yield表达式
调用函数返回一个指向内部状态的指针,即遍历器对象。必须调用遍历器的next方法,使得指针移向下一个状态。
yield表达式就是暂停标志
function* g() {
yield 'hello'
yield 'world'
return 'haha'
}
const ee = g() // 函数并不会立即执行
console.log(ee) // g {<suspended>}
console.log(ee.next()) // {value: "hello", done: false}
console.log(ee.next()) // {value: "world", done: false}
console.log(ee.next()) // {value: "haha", done: true}
console.log(ee.next()) // {value: undefined, done: true}
遍历器对象{value: "hello", done: false}表示 value 是yield表达式的值,done: false表示遍历还没有结束
Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。
function* gg() {
console.log('666')
}
const g1 = gg()
setTimeout(() => {
g1.next() // 1s后输出666
}, 1000)
yield表达式只能用在 Generator 函数里面
Proxy
外界对目标对象的访问可以被 Proxy 拦截,进行过滤和改写,意为“代理器”
let proxy = new Proxy(target, handler)
- target 目标对象
- handler 配置对象
在 ES6 之前,我们可以使用Object.defineProperty去保护对象的私有属性。例如:
let sign = { _appid: '12345678', _appkey: '666', desc: 'zgh的密钥' }
Object.defineProperties(sign, {
_appid: {
writable: false
},
_appkey: {
writable: false
}
})
但是如果想对多个属性进行保护,就得对多个属性进行声明writable: false,显然很麻烦,这时就可以用 Proxy 来解决这个问题
Proxy 意味着我们代理了这个对象,该对象所有的属性操作都会经过 Proxy
let sign = { _appid: '123456', _appkey: '666', desc: 'zgh的密钥' }
let signProxy = new Proxy(sign, {
get(target, property, receiver) {
return target[property]
},
set(target, propName, value, receiver) {
if (propName !== 'desc') {
console.log('该属性是私有属性,不允许修改!')
} else {
target[propName] = value
}
}
})
console.log(signProxy._appid) // "123456"
signProxy._appkey = 'dd' // 该属性是私有属性,不允许修改!
console.log(signProxy._appkey) // "666"
这时依然可以直接修改 sign 对象,如果希望对象完全不可修改,可以直接将 sign 写到 Proxy 的 target
应用场景:
- 数据校检
- 属性保护
示例
数据类型验证
有一个记账的对象,记录着用户的存款金额,为了方便以后计算,要保证存入的数据类型必须为Number
let account = { num: 8888 }
let proxyAccount = new Proxy(account, {
get(target, property) {
return target[property]
},
set(target, propName, propValue) {
if (propName === 'num' && typeof propValue != 'number') {
throw new TypeError('The num is not an number')
}
target[propName] = propValue
}
})
proxyAccount.num = '666'
console.log(proxyAccount.num) // Uncaught TypeError: The num is not an number
空值合并运算符
写法:a ?? b,如果第一个参数是null/undefined,则??返回第一个参数,否则返回第二个参数。
效果等同于(a !== null && a !== undefined) ? a : b
let a
let b = a ?? 1 // 1
??运算符的优先级非常低,仅略高于?和=,使用时要考虑是否添加括号- 如果没有明确添加括号,不能将其与
||或&&一起使用
与||的区别
||返回第一个真值??返回第一个已定义的值
|| 无法区分 false、0、空字符串""、NaN和null/undefined
let a = 0
a || 1 // 1
a ?? 1 // 0
可选链
当位于 ?. 前面的值为 undefined 或 null 时,会立即阻止代码的执行,并返回 undefined
const obj = { name: 'zgh' }
obj?.a
三种形式:
obj?.pronobj?.[pron]obj.method?.()