JavaScript深度挖掘之ECMAScript
0.闲聊
说实话啊,es6出来很久了,但是我在工作中发现很多小伙伴还是不会使用es6中的一些新特性来优化自己的代码。作为一名乐于助人的大哥哥,我是心急如焚,心如刀绞,心如死灰,于是匆匆写下这篇文章,希望大家看完能有收获,如果由大佬也希望补充自己的观点,毕竟我一个人百度到的辛苦整理的也有很大可能并不全面,指针在此拜谢。(该文章并不是全面介绍es6,而是介绍一些es6在工作中能够提供给我们的便利)
1.啥是ECMAScript
ECMAScript是一种由Ecma国际(前身为欧洲计算机制造商协会,英文名称是European Computer Manufacturers Association)通过ECMA-262标准化的脚本程序设计语言。这种语言在万维网上应用广泛,它往往被称为JavaScript或JScript,但实际上后两者是ECMA-262标准的实现和扩展。简单来说,ECMAScript是JavaScript语言的国际标准,JavaScript是ECMAScript的实现。
以上是官方的回答,我简单总结一下,JS是ES的扩展,它在ES规定的语法上进行了扩展,使得它能够在浏览器环境去操作DOM,BOM,在node环境去读写文件等等。
在web环境,ES + webApI(BOM,DOM操作) 就是JS
在node环境,ES + NodeApI(fs,net,etc.)就是JS
2.不管别的,先用起来
熟能生巧这句话在哪里都是适用的,很多时候你看一个东西你是记不住的,你必须得用起来,用着用着它就成为了你的习惯,所以我们要抛掉光用眼睛看的坏习惯,一定要手敲。
还有人会想,es6是2016年的,现在都2021年了,我这学得过来吗?别怕,官方就是怕大家学不过来,所以决定做一个年更up主(才不是为了托更呢),为此他们连版本号都放弃了,每次的新版本都以ECMAScript加年份来命名,所以我们常说的es6其实是2015年发布的。
2.1 关于声明变量的优化(let,const)
相信大家一定被var导致的变量提升相关的面试题折磨过,所以答应我,不要成为自己最讨厌的人,好吗?
// let 带来了JS中的第三个作用域,块级作用域。let所声明的变量只能在块级作用域中使用
// 以前的var声明的变量可以在声明之前使用,只不过是undefined而已,其实有点离谱,但是有个好听的名字叫变量提升,但是在let中,就不会有这样的问题,let定义的变量在未声明之前是会报错的。使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称TDZ)
console.log(a) // undefined
var a = 1
console.log(a2) // ReferenceError: Cannot access 'a2' before initialization
let a2 = 2
----------------------------------------------------------------------------
// let在工作中对我最大的帮助就是在使用for循环的时候
// 我们使用for i循环其实就是为了获取i,但是在let没有出来之前,我们经常会碰到这样的问题
// 因为for循环是同步,所以一但循环内部有异步代码时,因为for循环已经完成,所以i全部是5
for(var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, 0)
}
// 5 5 5 5 5
// 但是let就可以生成一个块级作用域,在这个作用域内,i是不变的
// 循环5次,就会生成五个块级作用域,每个作用域内的i互不影响
for(let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, 0)
}
// 0 1 2 3 4
----------------------------------------------------------------------------
// let声明的变量也不可以重复声明,防止后面声明的变量覆盖前面的
var a = 1
var a = 2
let b = 1
let b = 2 // SyntaxError: Identifier 'b' has already been declared
----------------------------------------------------------------------------
// 块级作用域必须有{}包着,所以一些类似if的简洁写法,并不会生成块级作用域
let a = 1
if(true) let a = 2 // SyntaxError: Lexical declaration cannot appear in a single-statement context
// 这样是可以的
if(true) {
let a = 2
}
----------------------------------------------------------------------------
// const很多和let一样,但是它定义了一个常量
// const定义的变量必须在定义时赋值,且不可更改
// 但是这里的不可更改指的是不可更改内存地址,所以对象和数组只要不改变变量内存指向的操作还是可行的
const a; // SyntaxError: Missing initializer in const declaration
const a = 1;
a = 2; // TypeError: Assignment to constant variable.
const obj = {}
obj.name = "zhizhen" // 可行,因为没有改变obj的内存指向
obj = {} // TypeError: Assignment to constant variable. 给obj重新赋值,改变了内存指向,所以不行
2.2 关于同时声明多个变量的优化(解构赋值)
解构赋值挺简单的,具体的看看怎么用,我觉得一看就会
左右对称,名字对称,不对称取不到,为undefined
// 普通优化
let a = 1;
let b = 2; => let [a, b, c] = [1, 2, 3]
let c = 3;
----------------------------------------------------------------------------
// 数组嵌套结构
let arr = ["zhizhen", ["male", "hanson", "clever"]]
let [name, nature] = arr
console.log(name) // "zhizhen"
console.log(nature) // ["male", "hanson", "clever"]
----------------------------------------------------------------------------
// 对象的解构赋值,常用于导入第三方库的时候只使用其中一部分
// 这样就导入了lodash的fp模块的curry方法
const { curry } = require("lodash/fp")
----------------------------------------------------------------------------
// 对象嵌套
let res = {
code: 200,
data: ["Merci", "Zoey"],
message: "success"
}
let { code, data, message1 } = res
console.log(code) // 200
console.log(data) // ["Merci", "Zoey"]
console.log(message1) // undefined
----------------------------------------------------------------------------
// 冒泡排序里优化赋值
let array=[3,6,2,2,5,7,8,1];
let length = array.length
for (let i = 0; i < length - 1; i++){
for (let j = 0; j < length - 1 - i; j++) {
if (array[j] > array[j + 1]) {
// let temp = array[j];
// array[j] = array[j + 1];
// array[j + 1] = temp;
[array[j], array[j+1]] = [array[j+1], array[j]]
}
}
}
----------------------------------------------------------------------------
// 实现函数的命名参数(类似吧)
function fn({ x, y, z }) {
console.log(x, y, z)
}
fn({ x: 1, y: 2, z: 3 })
2.3 字符串拼接优化(模板字符串)
let a = "hello"
let b = "zhizhen"
// 以前
let c = a + b + "l love you"
// 现在
// 使用``包裹整个字符串,使用${}可以将参数包裹
// ``可以直接换行
let c2 = `${a} ${b} l love you`
2.4 字符串的一些方法
// includes,startsWith,endsWidth都是查询字符串包含某字符的,返回bool值
// padStart和padEnd方法用于在字符串头部或尾部补全给定值,我常用于2进制的补0
let num = 5
let str = num.toString(2)
console.log(str); // 101
console.log(str.padStart(32, 0)); // 00000000000000000000000000000101
2.5 函数一些优化
// 箭头函数
// 不会吧不会吧,不会有人2021年了还不会用箭头函数吧
// => 最大的特性是,没有自身的this,会使用外部的this
// 这样可以避免很多因为this指向带来的问题
let fn = (value) => {
}
----------------------------------------------------------------------------
// fn.length获取没有默认值的函数参数个数,函数柯里化的时候有用到
function myCurry(fn) {
return function curryFn() {
if(arguments.length < fn.length) {
let arr = Array.from(arguments)
return function () {
let arr2 = Array.from(arguments)
return curryFn.apply(this, arr.concat(arr2))
}
}
return fn.apply(null, arguments)
}
}
----------------------------------------------------------------------------
// rest参数用于获取函数的多余参数,用于替代arguments对象。这样我们可以优化上面的代码
function myCurry(fn) {
return function curryFn(...args) {
if(args.length < fn.length) {
return function(...args2) {
return curryFn(...args.concat(...args2))
}
}
return fn(...args)
}
}
// rest代表剩余参数,所以如果方法中有其他参数,rest要放在最后
function fn(a, ...rest) {
console.log(rest)
}
fn(1, 2, 3, 4, 5) // [2, 3, 4, 5]
2.6 数组的一些优化方法
// 还是那个rest方法,这里叫扩展运算符,可以将数组转化为逗号分隔的参数序列
// 可以用于生成数组,复制数组,合并数组
let arr = [1, ...arr2]
let arr2 = [...arr]
let arr3 = [...arr, ...arr2]
----------------------------------------------------------------------------
// Array.from将伪数组转换成数组。上面柯里化的例子里有用到,es5的写法Array.slice.call(arrayLike)
// Array.from还可以接受第二个参数,作用类似于map方法
function fn() {
let arr = Array.from(arguments, (value) => value + 1)
console.log(arr)
}
fn(1,2,3) // 2,3,4
----------------------------------------------------------------------------
// find, findIndex都是用于寻找符合的数组成员
// includes判断数组中是否包含某个值
// filter过滤数组
// map遍历数组,通过某些操作,返回一个新数组
// 答应我,学会了之后别再一直for(let i = 0; i<len; i++)了好吗
let arr = [1, 2, 3]
let has3 = arr.find(item => item == 3)
console.log(has3); // 3
let has1 = arr.findIndex(item => item == 1)
console.log(has1); // 0
let has2 = arr.includes(2)
console.log(has2); // true
let arr2 = arr.filter(item => item > 1)
console.log(arr2); // [2, 3]
let arr3 = arr.map(item => item+1)
console.log(arr3); // [2, 3, 4]
2.7 对象的一些优化
// 变量名和属性名一致时,可以简写
// 你不一定要这么写,但是别人这么写,你得认识
let x = "hello"
let foo = { let foo = {
x: x => x
} }
// 方法也可以简写
let foo = {
sayHello: function () {
}
}
=>
let foo = {
sayHello() {
}
}
----------------------------------------------------------------------------
// 使用[]表达式作为属性名
let a = "hello"
let b = "sayHello"
let foo = {
[a]: "aaaa",
[b]() {
}
}
----------------------------------------------------------------------------
// 对象的遍历
Object.keys(obj) // 一般够用了,返回中不包括Symbol属性和不可枚举属性
Object.getOwnPropertyNames(obj) // 返回中不包括不可枚举属性
Reflect.ownKeys(obj) // 全部属性都返回
----------------------------------------------------------------------------
// Object.assign(target, obj1, obj2...)对象合并
// 第一个参数是目标参数,后面的都是被合并的对象,如果有同名参数,会使用后面的obj里的同名参数
let obj1 = {
a: 111,
b: 222
}
let obj2 = {
a: "obj2的a",
c: 333
}
Object.assign(obj1, obj2)
console.log(obj1); // { a: 'obj2的a', b: 222, c: 333 }
2.8 异步调用(Promise,Generator,async/await)
说实话啊,在我有限的工作生涯中,我没见过回调地狱,因为没碰到过那么多层的回调地狱,一般我用callback就解决了。但是呢,Promise等等新的解决异步调用的方法,你可以不用,但是别人写了,你一定要看得懂。多的不说,看我前两期文章就知道啦😀
2.9 class类生成构造函数
以前生成实例对象都是用的构造函数,但是写起来太难受了,所以有了class语法糖,用起来舒服,你也不用对class了如指掌,你把我下面说的几样了解,我觉得工作中够用了
class的基本使用
// 1.使用class关键字生成一个叫People的类
// 2.class中的constructor就是构造函数方法,可以接收参数,声明类的实例属性
// 3.class的构造方法会在new一个实例对象时执行
// 4.声明实例属性也可以不在构造函数中(如果报错请升级node版本),前面不需要var,let,const
// 5.声明一个实例方法,前面不需要加function,相当于在People.prototype.sayHello = function() {}
// 6.方法与方法,方法与属性之间不需要加,
// 7.静态属性,静态方法前加上static关键字,静态属性只能在静态方法中使用,静态方法由类直接调用
class People {
constructor(name, age) {
this.name = name
this.age = age
console.log("People的构造方法执行");
}
status = "health"
sayHello() {
console.log(`my name is ${this.name}, i am ${this.status}`)
}
static print() {
console.log("i am static fn")
}
static gender = "male"
}
let p = new People("指针", "18")
p.sayHello() // my name is 指针, i am health
People.print() // i am static fn
class的继承
// 1.使用extends关键字继承
// 2.构造方法里可以接受其他参数,专属于自己的参数
// 3.构造方法里先执行super,这是因为必须父类的构造方法执行了以后才会有属性方法去继承
// 4.可以声明自己的属性与方法
// 5.静态方法会继承过来
class Student extends People {
constructor(name, age, homework, homework2) {
super(name, age)
this.homework = homework
this.homework2 = homework2
console.log("Student的构造方法执行")
}
doHomeWork() {
console.log(`我爱做${this.homework},还有${this.homework2}`)
}
}
let stu = new Student("Merci", "18", "五三", "王后雄教案")
stu.sayHello() // my name is Merci, i am health
stu.doHomeWork() // 我爱做五三,还有王后雄教案
Student.print() // i am static fn
class利用new.target模拟抽象类
// new.target的特性是Class内部调用,返回当前Class
// 子类继承父类时,new.target会返回子类Class
// 让我们用这个特性模拟一个抽象类
class People {
constructor() {
if (new.target === People) {
throw new Error("抽象类不可以被实例化")
}
}
}
2.10 Set来处理数组去重
对于Set我只用到这个属性,如果大家还有别的,可以留言告知
// Set是一个类数组,但是成员都是唯一的,我们就是利用它这个特性实现数组去重
// 先使用new Set将数组转化成一个去重的Set集合
// 再用Arrry.from将Set集合转化成数组
let arr = [1,2,3,2,3,4,1]
arr = new Set(arr)
console.log(arr); // Set(4) { 1, 2, 3, 4 }
arr = Array.from(arr)
console.log(arr); // [ 1, 2, 3, 4 ]
2.11 Proxy和Reflect
因为Vue现在采用了Proxy做数据劫持,所以我觉得面试官一定会问的,所以Proxy你需要知道,还有Vue以前用的是Object.defineProperty,这两者的区别我觉得你也得了解一下
// Object.defineProperty方法有三个弊端,从它的入参你就能看出来
// 第一个参数是对象,第二个是对象属性,第三个是定义的描述
// 所以这三个弊端就是:
// 1.无法监听数组的变化
// 2.只能监听对象的单一属性变化,如果要监听对象,需要遍历监听
// 3.如果对象属性仍为对象,则需要深度遍历去监听
Object.defineProperty(obj, prop, descriptor)
// 我看再看看Proxy的入参
// 第一个参数是任意类型的对象,第二个参数是定义描述
// 由此我们可知,Proxy监听的是整个对象,也就避开了Object.defineProperty的弊端
new Proxy(target, handler)
// Proxy定义了13个方法(有兴趣的可以都了解一下)
// 给目标对象改变this指向
Proxy.apply(target, thisArg, args)
// 调用构造函数
Proxy.construct(target, args)
// 查找对象的属性
Proxy.get(target, name, receiver)
// 设置对象的属性
Proxy.set(target, name, value, receiver)
// 为对象定义属性
Proxy.defineProperty(target, name, desc)
// 删除对象属性
Proxy.deleteProperty(target, name)
// 判断对象中包含某属性
Proxy.has(target, name)
// 返回对象的所有属性
Proxy.ownKeys(target)
// 表示对象是否可扩展
Proxy.isExtensible(target)
// 将对象变为不可扩展
Proxy.preventExtensions(target)
// 得到指定属性的描述对象
Proxy.getOwnPropertyDescriptor(target, name)
// 读取对象的__proto__属性
Proxy.getPrototypeOf(target)
// 设置对象的原型
Proxy.setPrototypeOf(target, prototype)
而Reflect也定义了13方法,与Proxy的一一对应,Reflect实际上是为了统一对象相关的方法,将对象的所有方法统一成Reflect.xxx的形式,方便大家记忆,其实每个方法都可以找到替代的方法。
Reflect.apply(target, thisArg, args) => Object.prototype.toString.call(youngest)
Reflect.construct(target, args) => new MyClass()
Reflect.get(target, name, receiver) => 就是对象.属性名
Reflect.set(target, name, value, receiver) => 就是obj.xx = xxx
Reflect.defineProperty(target, name, desc) => Object.defineProperty
Reflect.deleteProperty(target, name) => delete obj.xx
Reflect.has(target, name) => key in obj
Reflect.ownKeys(target) => Object.getOwnPropertyNames + Object.getOwnPropertySymbols
Reflect.isExtensible(target) => Object.isExtensible
Reflect.preventExtensions(target) = Object.preventExtensions
Reflect.getOwnPropertyDescriptor(target, name) => Object.getOwnPropertyDescriptor
Reflect.getPrototypeOf(target) => Object.getPrototypeOf(obj)
Reflect.setPrototypeOf(target, prototype) => Object.setPrototypeOf(obj)
2.12 Symbol
这个我在工作中没用过,只知道是js新的数据类型,可以生成一个不可重复的值,这样在模块化中可以防止后来的方法覆盖之前的方法。工作中没用到过,大家仅作了解吧。