复习回顾 ECMAScript 标准
- ECMAScript 概述
- ECMAScript 2015
ECMAScript 概述
内容比较长,可能有纰漏,以下是正文
通常情况下,我们会有ECMAScript看做是JavaScript的标准化规范,但是实际上 JavaScript 是 ECMAScript 的扩展语言
ECMAScript 只提供了最基本的语法,它只是停留在语言层面,规范如果定义变量,如何去写循环等等。并不能直接用来去完成实际应用中的功能开发。
JavaScript 语言本身指的就是 ECMAScript。
JavaScript 实现了ECMAScript语言的标准, 并且在这之上做了其他扩展。使得 js 可以在浏览器平台下 可以操作dom ;在node 平台下可以读写文件 等等
从 2015年开始,ECMAScript 保持每年一个大版本的更新。
ECMAScript 2015
概述
其实就是常听到的ES6,之前的版本是ES5.1,ES2015 改变了命名方式但是很多人习惯性称呼ES2015为ES6。
也可以把 ES6 去泛指所有的 ESCMAScript 2015之后的新标准
比如经常听到这种说法
“使用ES6的async/await” 而实际上,这是ES2017的内容
ES2015 的新标准主要体现在以下四个方面:
- 解决原有语法上的一些问题或者不足
- 对原有语法进行增强
- 全新的对象、全新的方法、全新的功能
- 全新的数据类型和数据结构
新标准
let 与块级作用域
作用域: 指某个成员能够起作用的范围
在ES2015之前 js 只有两种作用域:全局作用域和函数作用域
在ES2015中,新增了块级作用域
if (true) {
console.log('hello')
}
for (var i = 0; i<10; i++) {
console.log('hello')
}
在if 和 for 的{ } 中都会产生块
块 指的就是一对花括号包裹起来的范围
通过 let 声明的成员只会在块级作用域中生效
根据 let 的特点,我们可以在在循环绑定事件的时候 使用let,这样可以在事件处理函数中获得正确的索引
// 伪代码
var elements = [{元素1}, {元素2}, {元素3}]
for (let i = 0; i < elements.length; i++) {
elements[i].onclick = function () {
console.log(i)
}
}
elements[0].onclick()
let 修复了变量声明提升现象。
关于let的提升问题存在一些争议
let x = 'global'
{
console.log(x) // Uncaught ReferenceError: x is not defined
let x = 1
}
从上面代码看 似乎是没有提升。
MDN 关于 let 是否存在提升的章节,被编辑了两次,第一次说存在提升,第二次说不存在提升(参考 2017 年 3 月 10 号的变更记录)。也就是说 MDN 的维护者都在这个问题上产生过分歧,更何况我们了。
ES 文档里出现了「var/let hoisting」字样。
鉴于此,我认为应该尊重 ES 文档,认为 let 确实存在提升。只不过由于暂时死区的限制,你不能在 let x 之前使用 let
我个人的理解是,js中变量的创建过程可以分为三个阶段: 创建create、初始化initialize 、赋值assign
var val = 123
// 定义一个变量 分成三个步骤
// 1. 创建val变量
// 2. 初始化val的值为 undefined
// 3. 对val进行赋值 val = 123
在var 中 对创建 和 初始化进行了提升,而let 只对创建做了提升。
const
常量是块级范围的,非常类似用 let 语句定义的变量。
- 常量声明过后不允许重新赋值
- 常量在声明时就必须同时进行赋值
- 常量对于复杂类型只是要求内层指向不能被修改,如果是内部成员修改是没有问题的
const obj = {}
obj.name = 'zs' // no problem
obj = {} // no !
数组和对象的解构
解构 (destructuring)
数组解构赋值在绑定值时根据属性下标和进行匹配的。赋值操作符(等号)两边的解构要一致才能正确赋值,如果匹配不成功则取值为undefined。
- 基本用法
const arr = [100, 200, 300]
const foo = arr[0]
const bar = arr[1]
const baz = arr[2]
console.log(foo, bar, baz) // 100 200 300
- 跳跃式的赋值,即获取某一个具体位置的值
const [, , baz] = arr
console.log(baz) // 300
- 获取数组剩下所有的元素, 通过…来获取,但是只能写在最后一个元素的位置上
const [foo, ...rest] = arr
console.log(rest) // [200,300]
- 设置默认值,对于没有匹配到 或者匹配到的属性值为undefined的变量可以指定默认值
const [foo, bar, baz = 123, more = 'default value'] = arr
console.log(bar, more) // 200 default value
对象解构是根据属性名来进行提取,而不是位置
- 基本用法
const obj = { name: 'zs', age: 18 }
const { name } = obj // 提取name属性值 放到一个name变量中
console.log(name) // 'zs'
- 解构属性重命名
const { name: objName } = obj
console.log(objName)
- 添加默认值
const { name: objName = 'jack' } = obj
console.log(objName)
模板字符串
ES2015中提出可以使用反引号来创建模板字符串
`string`
如果直接使用跟普通字符串没有什么区别,但是它比普通字符串多了很多新功能
- 支持换行,多行字符串
- 插入表达式
var a = 5;
var b = 10;
console.log(`Fifteen is ${a + b} and
not ${2 * a + b}.`);
// "Fifteen is 15 and
// not 20."
- 标签模板
const name = 'tom'
const gender = false
function myTagFunc (strings, name, gender) {
// 这里的strings 就是模板字符串中静态的字符串组成的数组
console.log(strings) // ['hey',' is a']
const sex = gender ? 'man' : 'woman'
// 标签函数的返回值就是模板字符串的返回值
return strings[0] + name + strings[1] + sex + strings[2]
}
}
const result = myTagFunc`hey,${name} is a ${gender}`
字符串扩展方法
includes()
判断字符串中是否包含参数
startsWith()
判断字符串是否以参数开头
endsWith()
判断字符串是否以参数结尾
const message = 'Error: foo is not defined.'
console.log(
message.startsWith('Error')
message.endsWith('.')
message.includes('foo')
)
参数默认值,剩余参数
参数默认值 (parmeter-defaults)
函数默认参数允许在没有值或undefined被传入时使用默认形参,带默认值的参数必须写在最后一个
// 默认参数一定是在形参列表的最后
function foo (enable = true) {
console.log('foo invoked - enable: ')
console.log(enable)
}
剩余参数(Rest Parameters)
剩余参数语法允许我们将一个不定数量的参数表示为一个数组
function foo (first, ...args) {
console.log(args) // [1,2,3,4]
}
foo(1, 2, 3, 4)
展开数组(Spread)
… 修饰符 可以用来展开数组
const arr = ['foo', 'bar', 'baz']
console.log(...arr) // foo,bar,baz
箭头函数 (Arrow Functions)
箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数
- 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this
- 当箭头函数只有一个参数时,可以省略参数的圆括号
- 当箭头函数的函数体只有一个
return
语句时,可以省略return
关键字和方法体的花括号
对象字面量增强 (Enhanced object literals)
- 属性名与变量名相同,可只写属性名
- 成员是方法,可以省略function
- 通过[] 让表达式结果作为属性名
// 对象字面量
const bar = '345'
const obj = {
foo: 123,
// bar: bar
// 属性名与变量名相同,可以省略 : bar
bar,
// method1: function () {
// console.log('method111')
// }
// 方法可以省略 : function
method1 () {
console.log('method111')
// 这种方法就是普通的函数,同样影响 this 指向。
console.log(this)
},
// Math.random(): 123 // 不允许
// 通过 [] 让表达式的结果作为属性名
[bar]: 123
}
// obj[Math.random()] = 123
console.log(obj)
obj.method1()
Object.assign
Object.assign(target, …sources) 方法,可以将多个源对象中的属性复制到一个目标对象中,返回目标对象。
Object.assign是浅拷贝
function func (obj) {
// obj.name = 'func obj'
// console.log(obj)
const funcObj = Object.assign({}, obj)
funcObj.name = 'func obj'
console.log(funcObj)
}
const obj = { name: 'global obj' }
func(obj)
console.log(obj)
Object.is
Object.is() 方法判断两个值是否为同一个值。
console.log(
// 0 == false // => true
// 0 === false // => false
// +0 === -0 // => true
// NaN === NaN // => false
// Object.is(+0, -0) // => false
// Object.is(NaN, NaN) // => true
)
正常来讲……用不到 === 足矣
Proxy,对比defineProperty
Object.defineProperty 方法可以用来监听对象属性的读写,在Vue3之前的版本中 就是通过它来实现数据响应,
在ES2015中新增了Proxy 专门用来为对象设置访问代理器。什么叫代理,简单来讲就是对象的访问交给一个中间人去完成 不用亲自去操作
相比于defineProperty,Proxy功能更加强大也更方便。
- Proxy基本用法
const person = {
name: 'zce',
age: 20
}
const personProxy = new Proxy(person, {
// 监视属性读取
// target 目标对象 这里指的就是person
// property 要访问的目标对象的属性
get (target, property) {
return property in target ? target[property] : 'default'
// console.log(target, property)
// return 100
},
// 监视属性设置
// target 目标对象 这里指的就是person
// property 要访问的目标对象的属性
// value是新值
set (target, property, value) {
if (property === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError(`${value} is not an int`)
}
}
target[property] = value
// console.log(target, property, value)
}
})
personProxy.age = 100
personProxy.gender = true
console.log(personProxy.name)
- 对比Proxy与defineProperty
- Proxy 可以监视读写以外的操作
const person = { name: 'zce', age: 20 } const personProxy = new Proxy(person, { deleteProperty (target, property) { console.log('delete', property) delete target[property] } }) delete personProxy.age console.log(person)
- Proxy 可以很方便的监视数组操作
const list = [] const listProxy = new Proxy(list, { set (target, property, value) { console.log('set', property, value) target[property] = value return true // 表示设置成功 } }) listProxy.push(100) listProxy.push(100)
- Proxy 不需要侵入对象,方式更为合理
const person = {} // 数据劫持需要设置一个内部属性_name来转存数据 Object.defineProperty(person, 'name', { get () { console.log('name 被访问') return person._name }, set (value) { console.log('name 被设置') person._name = value } }) Object.defineProperty(person, 'age', { get () { console.log('age 被访问') return person._age }, set (value) { console.log('age 被设置') person._age = value } }) person.name = 'jack' console.log(person.name) // Proxy 方式更为合理 const person2 = { name: 'zce', age: 20 } const personProxy = new Proxy(person2, { get (target, property) { console.log('get', property) return target[property] }, set (target, property, value) { console.log('set', property, value) target[property] = value } }) personProxy.name = 'jack' console.log(personProxy.name)
- Proxy 可以监视读写以外的操作
Reflect
Reflect 是一个内置的对象,内部封装了一系列对对象的底层操作。Reflect不是一个函数对象,因此它是不可构造的。
Reflect 成员方法就是Proxy处理对象的默认实现 (意思就是如果Proxy没有实现get 相当于默认调用了 Reflect.get)
- Reflect.apply(target, thisArgument, argumentsList) 对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和 Function.prototype.apply() 功能类似。
- Reflect.construct(target, argumentsList[, newTarget]) 对构造函数进行 new 操作,相当于执行 new target(…args)。
- Reflect.defineProperty(target, propertyKey, attributes) 和 Object.defineProperty() 类似。如果设置成功就会返回 true
- Reflect.deleteProperty(target, propertyKey) 作为函数的delete操作符,相当于执行 delete target[name]。
- Reflect.get(target, propertyKey[, receiver]) 获取对象身上某个属性的值,类似于 target[name]。
- Reflect.getOwnPropertyDescriptor(target, propertyKey) 类似于 Object.getOwnPropertyDescriptor()。如果对象中存在该属性,则返回对应的属性描述符, 否则返回
undefined.- Reflect.getPrototypeOf(target) 类似于 Object.getPrototypeOf()。
- Reflect.has(target, propertyKey) 判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。
- Reflect.isExtensible(target) 类似于 Object.isExtensible().
- Reflect.ownKeys(target) 返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys(), 但不会受enumerable影响).
- Reflect.preventExtensions(target) 类似于 Object.preventExtensions()。返回一个Boolean。
- Reflect.set(target, propertyKey, value[, receiver]) 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
- Reflect.setPrototypeOf(target, prototype) 设置对象原型的函数. 返回一个 Boolean, 如果更新成功,则返回true。
Reflect 我觉得它的意义在于统一提供了一套用于操作对象的API
const obj = {
name: 'zce',
age: 18
}
// console.log('name' in obj)
// console.log(delete obj['age'])
// console.log(Object.keys(obj))
console.log(Reflect.has(obj, 'name'))
console.log(Reflect.deleteProperty(obj, 'age'))
console.log(Reflect.ownKeys(obj))
Promise
这里不再赘述Promise了,可以参考我的另一篇博客 JavaScript 异步编程
class 类
替代之前的构造函数创建类的方式
- 基本用法
class Person {
// 构造器
constructor (name) {
// 实例成员
this.name = name
}
// 原型方法
say () {
console.log(`hi, my name is ${this.name}`)
}
}
const p = new Person('tom')
p.say()
- extends 继承
class Person {
constructor (name) {
this.name = name
}
say () {
console.log(`hi, my name is ${this.name}`)
}
}
class Student extends Person {
constructor (name, number) {
super(name) // 父类构造函数 , 要在this之前使用
this.number = number
}
hello () {
super.say() // 调用父类成员
console.log(`my school number is ${this.number}`)
}
}
const s = new Student('jack', '100')
s.hello()
static 静态方法
只能通过类名访问
// static 方法
class Person {
constructor (name) {
this.name = name
}
say () {
console.log(`hi, my name is ${this.name}`)
}
static create (name) {
return new Person(name)
}
}
const tom = Person.create('tom')
tom.say()
set
set 是ES2015提供的一种全新的数据结构,可以理解为集合,与数组非常类似。
与数组不同的是,set 中的值是不允许重复的,每个值都是唯一的
// Set 数据结构
// 创建集合实例
const s = new Set()
// 通过add方法新增成员,并且add返回集合实例 所以可以链式调用
s.add(1).add(2).add(3).add(4).add(2)
- 数组去重
// 数组去重
const arr = [1, 2, 1, 3, 4, 1]
// const result = Array.from(new Set(arr))
const result = [...new Set(arr)]
console.log(result)
- 循环遍历
s.forEach(i => console.log(i))
// 也可以使用ES2015中的 for…of 来遍历
for (let i of s) {
console.log(i)
}
- 常用的实例方法
Set.prototype.add(value)
在Set对象尾部添加一个元素。返回该Set对象
Set.prototype.clear()
移除Set对象内的所有元素。
Set.prototype.delete(value)
移除Set中与这个值相等的元素,返回Set.prototype.has(value)在这个操作前会返回的值(即如果该元素存在,返回true,否则返回false)。Set.prototype.has(value)在此后会返回false。
Set.prototype.has(value)
返回一个布尔值,表示该值在Set中存在与否
console.log(s.has(100))
console.log(s.delete(3))
console.log(s)
s.clear()
Map
Map数据结构与对象非常类似,本质上它们都是键值对集合,但是对象结构中的key只能是字符串类型
虽然我们平时在编码时,可能会把布尔值,对象作为一个对象的键,虽然它们本身不是字符串 但是对象内部会把这些属性toString,比如
const obj = {}
obj[true] = 'value'
obj[123] = 'value'
obj[{ a: 1 }] = 'value'
console.log(Object.keys(obj)) // ['123','true','[object Object]']
console.log(obj['[object Object]']) // value
- map 可以用来映射两个任意类型数据之间的对应关系,算是真正的集合
const m = new Map()
const tom = { name: 'tom' }
m.set(tom, 90)
console.log(m) // Map { { name: 'tom' } => 90 } 这里key就是tom这个对象 value就是90
console.log(m.get(tom)) // 90
- 常见的方法
Map.prototype.clear()
移除Map对象的所有键/值对 。
Map.prototype.delete(key)
如果 Map 对象中存在该元素,则移除它并返回 true;否则如果该元素不存在则返回 false。随后调用 Map.prototype.has(key) 将返回 false 。
Map.prototype.get(key)
返回键对应的值,如果不存在,则返回undefined。
Map.prototype.has(key)
返回一个布尔值,表示Map实例是否包含键对应的值。
- 循环遍历
m.forEach((value, key) => {
console.log(value, key)
})
- Map类型的构造函数可以将一个二维键值对数组转换成一个Map对象
console.log(new Map([['uname':'zs'],['age':12]]))
Symbol
ES2015 引入了一种新的原始数据类型 Symbol ,表示独一无二的值,最大的用法是用来定义对象的唯一属性名。
const s = Symbol()
console.log(s) // Symbol()
console.log(typeof s) // symbol
// 两个 Symbol 永远不会相等
console.log(
Symbol() === Symbol() // false
)
为了区分到底是哪一个symbol 我们可以个它传入一个描述字符串
// Symbol 描述文本
console.log(Symbol('foo'))
console.log(Symbol('bar'))
console.log(Symbol('baz'))
如果要获取一个添加了描述字符串的symbol我们需要通过 symbol的for方法
Symbol.for()
Symbol.for() 类似单例模式,首先会在全局搜索被登记的 Symbol 中是否有该字符串参数作为名称的 Symbol 值,如果有即返回该 Symbol 值,若没有则新建并返回一个以该字符串参数为名称的 Symbol 值,并登记在全局环境*搜索
只接受string类型参数 如果传入的不是string会自动转为string
const s1 = Symbol.for('foo')
const s2 = Symbol.for('foo')
console.log(s1 === s2) // true
const yellow = Symbol("Yellow");
const yellow1 = Symbol.for("Yellow");
yellow === yellow1; // false
从 ES2015开始,对象可以开始使用symbol作为属性key
const obj = {}
obj[Symbol()] = '123'
obj[Symbol()] = '456'
console.log(obj) // { [Symbol()]: '123', [Symbol()]: '456' }
可以使用Symbol为对象添加私有成员
// Symbol 模拟实现私有成员
// a.js ======================================
const static_name = Symbol()
const person = {
[static_name]: 'zce',
say () {
console.log(this[static_name])
}
}
// 只对外暴露 person
// b.js =======================================
// 由于无法创建出一样的 Symbol 值,
// 所以无法直接访问到 person 中的「私有」成员
// person[Symbol()]
person.say()
对象中的Symbol属性名 无法被for…in、Object.key()、JSON.stringify 这些手段获取到
如果要获取对象中的Symbol属性 可以使用 Object.getOwnPropertySymbols() 方法来获取
除了自己创建的symbol,JavaScript还内建了一些在ECMAScript 5 之前没有暴露给开发者的symbol,它们代表了内部语言行为,作为内部方法的标识,这些标识符可以让自定义对象去实现一些js内置的接口。
比如: Symbol.iterator Symbol.hasInstance 等等
for…of循环
可以作为遍历所有数据结构的统一方式,只要明白它的工作原理( 迭代器 )就可以使用它去遍历任何一种自定义的数据结构
- 基本使用
const arr = [100, 200, 300, 400]
// item 是数组中的每一个成员
for (const item of arr) {
console.log(item)
}
- for…of 循环可以替代 数组对象的 forEach 方法
arr.forEach(item => {
console.log(item)
})
// ==================================
for (const item of arr) {
console.log(item)
// 使用for of 可以配合break终止循环
if (item > 100) {
break
}
}
- for…of 遍历set 和 map
//遍历 Set 与遍历数组相同
const s = new Set(['foo', 'bar'])
for (const item of s) {
console.log(item)
}
//遍历 Map 可以配合数组结构语法,直接获取键值
const m = new Map()
m.set('foo', '123')
m.set('bar', '345')
// 遍历map 得到的是[key, value]结构的数组
for (const [key, value] of m) {
console.log(key, value)
}
- 普通对象不能被 for…of 遍历
const obj = { foo: 123, bar: 456 }
for (const item of obj) {
console.log(item)
}
可迭代接口
因为for…of 不能直接遍历普通对象,所以我们需要明白它遍历的原理,也就是可迭代的。
ES2015 提供了 Iterable 接口,只要数据结构实现了Iterable接口 就可以被 for of 进行遍历
- 所有能够被for…of遍历数据类型都要实现 Iterable 这个接口 (内部需要挂载一个Iterable方法) 即 Symbol.Iterable
- 这个方法需要返回一个带有next() 方法的对象,不断调用next就可以实现对内部数据的不断遍历
我们可以利用现有的可以直接被for of 遍历的数据结构 来证明这一点
const set = new Set(['foo', 'bar', 'baz'])
const iterator = set[Symbol.iterator]()
console.log(iterator.next()) //{ value: 'foo', done: false }
console.log(iterator.next()) //{ value: 'bar', done: false }
console.log(iterator.next()) //{ value: 'baz', done: false }
console.log(iterator.next()) //{ value: undefined, done: true }
console.log(iterator.next()) //{ value: undefined, done: true }
我们可以在set的原型上找到 Symbol.iterator
迭代器设计模式
const todos = {
life: ['吃饭', '睡觉', '打豆豆'],
learn: ['语文', '数学', '外语'],
work: ['喝茶'],
// 提供统一遍历访问接口
each: function (callback) {
const all = [].concat(this.life, this.learn, this.work)
for (const item of all) {
callback(item)
}
},
// 提供迭代器(ES2015 统一遍历访问接口)
[Symbol.iterator]: function () {
const all = [...this.life, ...this.learn, ...this.work]
let index = 0
return {
next: function () {
return {
value: all[index],
done: index++ >= all.length
}
}
}
}
}
todos.each(function (item) {
console.log(item)
})
console.log('-------------------------------')
for (const item of todos) {
console.log(item)
}
生成器(Generator)
- 生成器函数会返回一个生成器对象,
- 当我们调用这个生成器对象的next方法 才会开始直接函数体,
- 在执行的过程中一旦遇到了yiel关键词 函数的执行就会被暂停下来,
- yield 后面的值会作为 next方法的返回值的结果 { value:yield后面的值, done:false/true },
- 继续调用next就会从暂停的位置 继续执行
function * foo () {
console.log('1111')
yield 100
console.log('2222')
yield 200
console.log('3333')
yield 300
}
const generator = foo()
console.log(generator.next()) // 第一次调用,函数体开始执行,遇到第一个 yield 暂停
console.log(generator.next()) // 第二次调用,从暂停位置继续,直到遇到下一个 yield 再次暂停
console.log(generator.next()) // 。。。
console.log(generator.next()) // 第四次调用,已经没有需要执行的内容了,所以直接得到 undefined
ES2016
Array.prototype.includes
includes() 方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false。
const arr = ['foo', 1, NaN, false]
// 直接返回是否存在指定元素
console.log(arr.includes('foo'))
// 能够查找 NaN
console.log(arr.includes(NaN))
指数运算符
console.log(Math.pow(2, 10))
console.log(2 ** 10)
ES2017
Object.values
Object.values()方法返回一个给定对象自身的所有可枚举属性值的数组,值的顺序与使用for…in循环的顺序相同
Object.entries
Object.entries()方法 可以以数组的形式返回对象中所有的键值对
const obj = {
foo: 'value1',
bar: 'value2'
}
console.log(Object.entries(obj))
// 可以配合for of 来遍历对象
for (const [key, value] of Object.entries(obj)) {
console.log(key, value)
}
// 因为Map类型的构造函数可以将一个二维键值对数组转换成一个Map对象 所以可以使用entries把对象转化为Map对象
console.log(new Map(Object.entries(obj)))
Object.getOwnPropertyDescriptors
Object.getOwnPropertyDescriptors() 方法用来获取一个对象的所有自身属性的描述符(获取一个对象中属性的完整描述信息)
主要是为了解决 Object.assign() 无法正确拷贝 get 属性和 set 属性的问题
const p1 = {
firstName: 'Jim',
lastName: 'Green',
get fullName () {
return this.firstName + ' ' + this.lastName
}
}
// console.log(p1.fullName)
// const p2 = Object.assign({}, p1)
// p2.firstName = 'LiLei'
// console.log(p2)
const descriptors = Object.getOwnPropertyDescriptors(p1)
// console.log(descriptors)
const p2 = Object.defineProperties({}, descriptors)
p2.firstName = 'LiLei'
console.log(p2.fullName)
String.prototype.padStart / String.prototype.padEnd
padStart() 方法用另一个字符串填充当前字符串(如果需要的话,会重复多次),以便产生的字符串达到给定的长度。从当前字符串的左侧开始填充
padEnd() 方法会用一个字符串填充当前字符串(如果需要的话则重复填充),返回填充后达到指定长度的字符串。从当前字符串的末尾(右侧)开始填充
const books = {
html: 5,
css: 16,
javascript: 128
}
for (const [name, count] of Object.entries(books)) {
console.log(`${name.padEnd(16, '-')}|${count.toString().padStart(3, '0')}`)
}
html------------|005
css-------------|016
javascript------|128