let/const
var/函数
我们都知道var和函数存在变量提升的问题,它的声明会被提升到作用域的顶端。
if(true) {
var value = 1
}
console.log(value) //1
//代码相当于
var value
if(true) {
value = 1
}
console.log(value) //1
为了加强对变量生命周期的控制,ES 2015引入了块级作用域,let 和const。
块级作用域在于:
- 函数内部
- 块中的()字符和{}之间的区域
临时死区(Temporal Dead Zone)
简称TDZ,因为 JavaScript 引擎在扫描代码发现变量声明时,要么将它们提升到作用域顶部(遇到 var 声明),要么将声明放在 TDZ 中(遇到 let 和 const 声明)。访问 TDZ 中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从 TDZ 中移出,然后方可访问。
所以其实let 和const 也是有提升只是存放到了暂时性死区里,在没有变量声明之前访问报错。console.log(typeof value); // Uncaught ReferenceError: value is not defined
let value = 1;
看一个老生常谈的问题 for循环
相当于在 for(let i=0; i<3; i++) 有一个隐藏的作用域。for(let i=0; i<3; i++) {
let i = 'foo'
console.log(i)
}
// 拆分就是
let i = 0;
if(i<3) {
let i = 'foo'
console.log(i)
}
i++
if(i<3) {
let i = 'foo'
console.log(i)
}
i++
if(i<3) {
let i = 'foo'
console.log(i)
}
// 所以说for是双层作用域
// for(let ...)这是一层作用域
// 里面又是内层作用域
const
再来说下const,const 用于声明常量,其值一旦被设定不能再被修改,否则会报错。不允许声明之后修改内存地址 就是说,不允许修改绑定,但允许修改值这就意味着当const声明对象时const obj = {}
obj.name = 'tom'
//完全没有问题
建议
在我们开发中主用const,辅助用let,不用var。
变量的解构赋值
从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
数组的结构赋值
原来我们数组赋值必须指定值。
const arr = [1,2,3]
const f = arr[0]
const b = arr[1]
const c = arr[2]
ES2015 允许这样写
const arr = [1,2,3]
const [f, b, c] = arr
console.log(f,b,c)
//1 2 3
上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。
解构不成功
let [f] = [];
let [f, b] = [1];
如果解构不成功,变量的值就等于undefined。以上两种情况都属于解构不成功,f/b的值都会等于undefined.
不完全解构
另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。
const arr = [1,2,3]
const [f, b] = arr
console.log(f,b)
//1 2
默认值
解构赋值允许指定默认值。
const arr = [1,2,3]
const [f, b, c, d='more'] = arr
console.log(f,b,c,d)
这种数组的解构赋值很大程度上帮助缩短我们写代码逻辑,比如分割字符串获取字段
const path = 'foo/part/ccc'
const tmp = path.split('/')
const root = tmp[1]
console.log(root)
//之后
const [,root] = path.split('/')
console.log(root)
对象的解构赋值
解构不仅可以用于数组,还可以用于对象,
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined
上面代码的第一个例子,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的同名属性,导致取不到值,最后等于undefined。相当于解构失败,变量的值等于undefined。
其实,对象的解构赋值是下面形式的简写。
let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };
也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined
上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。
等以后深入的使用过程中,可以更加理解解构赋值,看个例子
let {log} = console
log('foo')
log('foo1')
log('foo3')
//foo
//foo1
//foo3
//大大简化了代码的编写 整体体积也减少了很多
相当于:
const {log:lo} = console //log:相当于模式 后面的lo才是变量
lo('fooo')
lo('foo1')
lo('foo3')
//foo
//foo1
//foo3
对象的解构也可以指定默认值,默认值生效的条件是,对象的属性值严格等于undefined。
var {x = 3} = {};
x // 3
var {x, y = 5} = {x: 1};
x // 1
y // 5
字符串的解构赋值
字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。
let {length : len} = 'hello';
len // 5
函数参数的解构赋值
函数的参数也可以使用解构赋值。
function add([x, y]) {
console.log(x+y)
}
add([1,2])
//3
另一个例子
function move ({x=0, y=0}) {
return [x, y]
}
move({x:3, y:8})
函数move的参数是一个对象,通过对这个对象进行解构,得到变量x和y的值。如果解构失败,x和y等于默认值。
一些用途
交换变量的值
let a = 1
let y = 2
[x, y] = [y, x]
从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。
/ 返回一个数组
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();
// 返回一个对象
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();
- 提取 JSON 数据 ```javascript let jsonData = { id: 42, status: “OK”, data: [867, 5309] };
let { id, status, data: number } = jsonData;
console.log(id, status, number); // 42, “OK”, [867, 5309]
上面代码可以快速提取 JSON 数据的值。<br />4.输入模块的指定方法
```javascript
const { SourceMapConsumer, SourceNode } = require("source-map");
模板字符串
定义
模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。
基本使用
// 普通字符串
`In JavaScript '\n' is a line-feed.`
// 多行字符串
`In JavaScript this is
not legal.`
console.log(`string text line 1
string text line 2`);
// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。
const {log} = console
const name = 'tom'
const msg = `hey,${name}---${Math.random()}------${1+2}`
log(msg)
//hey,tom---0.8797044709433715------3
模板字符串之中还能调用函数。
function fn() {
return "Hello World";
}
`foo ${fn()} bar`
// foo Hello World bar
标签模板
它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template),就是对字符串进行加工。
const {log} = console
const str = myTagFunc`hey,${name}is a ${gender}.`
function myTagFunc(string, name, gender) {
const sex = gender? 'man' : 'woman';
return string[0]+ name + string[1] + sex + string[2]
}
log(str)
//hey,tom is a man.
字符串的扩展方法
传统上,JavaScript 只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。
- includes():返回布尔值,表示是否找到了参数字符串。
- startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
//字符串的查找方法
const message = 'Error:foo is not defined.'
const {log} = console
log(
message.startsWith('Error'), //是否是以 Error开头
message.endsWith('.'), //是否是以 .结尾
message.includes('foo') //字符串中间是否包含 foo
)
repeat()方法,返回一个字符串,表示将原字符串重复n次
'x'.repeat(5)
//xxxxx
padStart(),padEnd()
如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
padStart()和padEnd()一共接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。
- trimStart(),trimEnd()
它们的行为与trim()一致,trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。
- replaceAll()
字符串的实例方法replace()只能替换第一个匹配
'aabbcc'.replace('b', '_')
// 'aa_bcc
上面例子中,replace()只将第一个b替换成了下划线。
如果要替换所有的匹配,不得不使用正则表达式的g修饰符。
'aabbcc'.replace(/b/g, '_')
// 'aa__cc'
正则表达式毕竟不是那么方便和直观, 引入了replaceAll()方法,可以一次性替换所有匹配。
'aabbcc'.replaceAll('b', '_')
// 'aa__cc'
剩余参数/展开数组
剩余参数
…只可出现在最后并且只有一个
const {log} = console
//...只可出现在最后并且只有一个
function foo(first, ...args) {
// log(Array.from(arguments))
log(args)
}
foo(1,2,3,4)
展开数组
//...展开数组
const arr = ['foo', 'bar', 'baz']
console.log.apply(console, arr)
// 同
console.log(...arr)
箭头函数
基本用法
让函数的书写变得很简洁,可读性很好。
我们看一个简单的例子:
function inc (number) {
return number + 1
}
const inc = (n,m)=> {
return n + 1
}
还有循环中能简化很多代码
const arr = [1,2,3,4,5]
const newArr = arr.filter(function(i) {
return i % 2
})
const newArr = arr.filter(i => i % 2 )
//[ 1, 3, 5 ]
我们注意一点 如果箭头函数的圆括号后面你省略了{},是默认return 内容得。加上{} 你就得写上return关键词。
箭头函数与this
前言this
在“use strict”严格模式下,没有直接的挂载者(或称调用者)的函数中this是指向window,这是约定俗成的。在“use strict”严格模式下,没有直接的挂载者的话,this默认为undefined。下面讨论的例子都是在非严格模式下。
this具有运行期绑定的特性,是基于函数的执行环境绑定的,在全局函数中,this指向的是window,当函数被作为某个对象调用时,this就等于那个对象。
箭头函数的this
箭头函数的this定义:箭头函数的this是在定义函数时绑定的,不是在执行过程中绑定的。简单的说,函数在定义时,this就继承了定义函数的对象。
例子:
var name = '333'
var person = {
name: 'tom',
sayHi:function() {
return function() {
return console.log(this.name)
}
}
}
person.sayHi()()
//333
//--------------------------------------
var name = '333'
var person = {
name: 'tom',
sayHi:function() {
setTimeout(function() {
console.log(this.name)
},1000)
}
}
person.sayHi()
//333
原因是,匿名函数的执行环境是全局的。this只在函数内部起作用。此时的this.name在匿名函数中找不到,所以就从全局中找,找到后打印出来。
原来我们常常通过闭包的特性缓存上下文this
// 箭头函数 与 this
var name = '333'
var person = {
name: 'tom',
syaHIAsync:function() {
const _this = this //通过闭包的特性
setTimeout(function() {
console.log(_this.name)
},1000)
}
}
person.syaHIAsync()
//tom
现在可以用箭头函数
// 箭头函数 与 this
var name = '333'
var person = {
name: 'tom',
syaHIAsync:function() {
setTimeout(()=> {
console.log(this.name)
},1000)
}
}
person.syaHIAsync()
//tom
箭头函数的this始终和它最近的外层相同的指向,这样就是箭头函数中的this。
对象字面量
ES6 新推出的新简写法,用来初始化对象并向对象添加方法。
我们经常这样写
// 年货糖果按斤计费
let type = 'candy';
let weight = '5';
let price = '8';
const goods = {
type: type,
weight: weight,
price: price
}
键值对出现了重复,ES2015 中,如果属性名和和所分配的变量名一样,就可以从对象属性中删掉这些重复的变量名称。
let type = 'candy';
let weight = '5';
let price = '8';
const goods = {
type,
weight,
price, // 如果属性名和字面量一致的话 可以省略
total: function(){
// ...
}
}
计算属性名
在之前ES5中,如果属性名是个变量或者需要动态计算,则只能通过 对象.[变量名] 的方式去访问。
而在字面量中是无法使用的。
const p = {
name : '李四',
age : 20
}
const attName = 'name';
console.log(p[attName]) //这里 attName表示的是一个变量名。
//p[attName] === p.name
//-----------------------------------------------
const p = {
attName : '李四', // 这里的attName是属性名,相当于各级p定义了属性名叫 attName的属性。
age : 20
}
console.log(p[attName]) // undefined
在ES2015中,计算属性名 [] 里面可以使用任意的表达式 作为这个属性的属性名称。
上述例子中就可以写为:
const p = {
[attName] : '李四', // 引用了变量attName。相当于添加了一个属性名为name的属性
age : 20
}
console.log(p[attName]) // 李四
计算属性名 [] 里面可以使用任意的表达式
// 对象字面量
const bar = '123'
const obj = {
foo: 12,
// bar: bar, //1
bar, //2
}
// 如果属性名和字面量一致的话 可以省略 如同 1 2 行一样
//计算属性名 [] 里面可以使用任意的表达式 作为这个属性的属性名称
obj[Math.random()] = 124
obj[1+2] = 3
console.log(obj)
//{ '3': 3, foo: 12, bar: '123', '0.40770536872805674': 124 }
对象新增方法
Object.assign()
基本用法:
Object.assign()方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
第一个参数是目标参数,把剩下的参数都合并到目标参数里,可以接收多个参数 同名会被覆盖。
//object.assign 方法
const source1 = {
a:function() {
return true
},
b:5,
c:4
}
const source2 = {
d:10,
e:11
}
const target = {
a: function() {
return false
},
f: 222
}
const result = Object.assign(target, source1, source2)
console.log(target.a()) //后面的属性会覆盖前面的属性
console.log(result === target)
//true
//true
注意:非对象参数出现在源对象的位置(即非首参数),首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果undefined和null不在首参数,就不会报错。在首参的话就会报错。
浅拷贝
Object.assign()方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
const obj1 = {a: {b: 1}};
const obj2 = Object.assign({}, obj1);
obj1.a.b = 2;
obj2.a.b // 2
Object.is()
ES5 比较两个值是否相等,只有两个运算符:相等运算符(==)和严格相等运算符(===)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。
Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致,不同之处只有两个:一是+0不等于-0,二是NaN等于自身。
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
Object.keys()
Object.values()
Object.entries()
Object.fromEntries()
proxy
前言:ES2015之前我们要监视对象属性的读写可以用Object.defineProperty(),但是有一些局限性,而ES2015提供了更好用的更为强大的方法proxy。下面我们来对比下。
1.object.definedProperty 只能监视到属性的读写,proxy 能监听到很多对象的操作。拿对象的删除举例
const person = {
name: 'tom',
age: 12
}
const personProxy = new Proxy (person, {
deleteProperty(target, property) { //目标对象、属性名
console.log(`delete: ${property}`)
delete target[property]
}
})
delete personProxy.age
console.log(person)
// delete: age
//{ name: 'tom' }
get方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。
2.object.definedProperty 是重写数组方法 像push等,而proxy是拦截对数组的操作,对数组的监视
const arr = []
const arrProxy = new Proxy(arr, {
set(target, property, value) { //目标对象、属性名、属性值
console.log('set', property, value)
target[property] = value
return true
}
})
arrProxy.push(100)
set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。
Proxy的拦截操作
下面是 Proxy 支持的拦截操作一览,一共 13 种。
get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy[‘foo’]。
set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy[‘foo’] = v,返回一个布尔值。
has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(…args)、proxy.call(object, …args)、proxy.apply(…)。
construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(…args)。
小总结
还是在工作中多用才能记住,相比较于Object.definedProperty(),proxy是以非侵入的方式监管了对对象的读写。
实例
get应用实例
const person = {
name: 'tom'
}
const personProxy = new Proxy(person, {
get:function(target, propKey) {
if(propKey in target) {
return target[propKey]
}else {
throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.");
}
}
})
proxy.name // "tom"
proxy.age // 抛出一个错误
set 应用实例
假定Person对象有一个age属性,该属性应该是一个不大于 200 的整数,那么可以使用Proxy保证age的属性值符合要求。
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// 对于满足条件的 age 属性以及其他属性,直接保存
obj[prop] = value;
}
};
let person = new Proxy({}, validator);
person.age = 100;
person.age // 100
person.age = 'young' // 报错
person.age = 300 // 报错
其他的不再一一举例了。
Class
基本用法
JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。
//旧的定义原型对象
function Person(name) {
this.name = name
}
Person.prototype.say = function() {
console.log(`hi, my name is ${this.name}`)
}
const person1 = new Person('tom')
person1.say()
在ES6 es2015中引入了 Class(类)这个概念,作为对象的模板 ,通过class关键字,可以定义类。
上面代码改写
class Person {
// 构造函数
constructor(name) {
this.name = name
}
say() {
console.log(`hi, my name is ${this.name}`)
}
}
const person1 = new Person('tom')
person1.say()
可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。也就是说,ES5 的构造函数Person,对应 ES6 的Person类的构造方法。
class的继承
class 可以通过extends关键字实现继承,这比es5的通过修改原型链实现继承,要清晰方便。下面我们来看个例子:
// extends 继承
class Person {
// 构造函数
constructor(name) {
this.name = name
}
say() { //实例方法
console.log(`hi, my name is ${this.name}`)
}
static create(name) { //静态方法 注意静态方法是通过构造函数直接调用的this指向没有改变
return new Person(name)
}
}
class Student extends Person {
constructor(name, number) {
super(name) //super永远指向父类,调用父类的constructor(name)
this.number = number
}
hello() {
super.say()
console.log(`my school number is ${this.number}`)
}
}
const student1 = new Student('rose', '111')
student1.hello()
//hi, my name is rose
//my school number is 111
上面代码中,constructor方法和hello方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。
- 子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,如果不调用super方法,子类就得不到this对象
- 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。
- 父类的静态方法,也会被子类继承。 ```javascript class A { static hello() { console.log(‘hello world’); } }
class B extends A { }
B.hello() // hello world
<a name="cHhDt"></a>
### Reflect
<a name="eCJTy"></a>
#### 概述
Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。
1. **将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上**。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
2. **修改某些Object方法的返回结果,让其变得更合理**。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
```javascript
// 老写法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
- 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。 ```javascript // 老写法 ‘assign’ in Object // true
// 新写法 Reflect.has(Object, ‘assign’) // true
// 老写法 const person = { name : ‘tom’ } delete person.name // 新写法 reflect.deleteProperty(person, name) //—————————————————————————— const {log} = console const obj = { name: ‘tom’, age: 12 } const reflect = Reflect.get(obj, ‘name’) const reflect2 = Reflect.deleteProperty(obj, ‘age’) const reflect3 = Reflect.has(obj, ‘name’) log(reflect,reflect2,reflect3)
4. **Reflect对象的方法与Proxy对象的方法一一对应**,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
```javascript
Proxy(target, {
set:function(target, propKey, value) {
var success = Reflect.set(target, propKey, value);
if(success) {
console.log('property ' + name + ' on ' + target + ' set to ' + value);
}else {
return success;
}
}
})
上面代码中,Proxy方法拦截target对象的属性赋值行为。它采用Reflect.set方法将值赋值给对象的属性,确保完成原有的行为,然后再部署额外的功能。
下面是另一个例子
var loggedObj = new Proxy(obj, {
get(target, name) {
console.log('get', target, name);
return Reflect.get(target, name);
},
deleteProperty(target, name) {
console.log('delete' + name);
return Reflect.deleteProperty(target, name);
},
has(target, name) {
console.log('has' + name);
return Reflect.has(target, name);
}
});
上面代码中,每一个Proxy对象的拦截操作(get、delete、has),内部都调用对应的Reflect方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。
Set/Map数据结构
set
基本用法ES2015 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
Set本身是一个构造函数,用来生成 Set 数据结构。
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
// Set(4) {2, 3, 5, 4}
for (let i of s) {
console.log(i);
}
// 2 3 5 4
Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
add 会返回集合方法本身 所以可以链式调用, 添加了存在的值 就会被忽略。
const s = new Set()
s.add(1).add(2).add(3).add(1)
//Set { 1, 2, 3 }
用来去重数组
const arr = [1,2,3,4,5,5,3]
const newArr = [...new Set(arr)]
console.log(newArr)
去重字符串
[...new Set('abckdeedsww')].join('')
Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是向 Set 加入值时认为NaN等于自身,而精确相等运算符认为NaN不等于自身。
另外,两个对象总是不相等的。
set的属性和方法
s.add(1).add(2).add(2);
// 注意2被加入了两次
s.size // 2
s.has(1) // true
s.has(2) // true
s.has(3) // false
s.delete(2);
s.has(2) // false
Array.from方法可以将 Set 结构转为数组。这就又提供了一个数组去重的方法
const newArr = Array.from(new Set([1,2,3,4,5,6,3,5]))
console.log(newArr)
set遍历操作
Set 结构的键名就是键值(两者是同一个值)
keys(),values(),entries()
let set = new Set(['red', 'green', 'blue']);
for (let item of set.keys()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]
//其实可以省略values直接遍历,Set 结构的实例默认可遍历
for (let item of set) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]
Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);
//并集
let union = new Set([...a],[...b])
// Set {1, 2, 3, 4}
//交集
let intersect = new Set([...a].filter(x=> b.has(x)))
// set {2, 3}
//差集
// (a 相对于 b 的)差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}
map
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
ES2015 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。
//对象
const obj = {}
obj[true] = 'value'
obj[123] = 'value2'
obj[{a:1}] = 'value3'
const {log} = console
log(obj)
//{ '123': 'value2', true: 'value', '[object Object]': 'value3' }
//可以看出 对象是调用键值的tostring.转化为string值
//---------------------------------------------------------------
const m = new Map()
const tom = {name: 'tom'}
m.set(tom, 100)
m.set(Math.random, '随机数')
log(m)
// Map { { name: 'tom' } => 100, 0.4260789016377904 => '随机数' }
log(m.get(tom))
// 100
for (const [key, value] of m) {
console.log(key, value)
}
//{ name: 'tom' } 100
//0.5114862235627582 随机数
Symbol
比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。ES2015就提供了Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。
let s = Symbol();
console.log(s)
变量s就是一个独一无二的值。typeof运算符的结果,表明变量s是 Symbol 数据类型。
- Symbol 是一个原始类型的值,不能用new
- Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。
- 可以接受一个字符串作为参数,为新创建的 Symbol 提供描述,用来显示在控制台或者作为字符串的时候使用,便于区分。 ```javascript const s = Symbol() console.log(typeof s) console.log( Symbol() === Symbol() ) //false //——————————————————-
console.log(Symbol(‘foo’)) console.log(Symbol(‘g’)) console.log(Symbol(‘tom’))
//Symbol(foo) //Symbol(g) //Symbol(tom)
//——————————————————-
let s1 = Symbol(‘foo’); let s2 = Symbol(‘foo’);
s1 === s2 // false
//——————————————————
//Symbol 值不能与其他类型的值进行运算,会报错 let sym = Symbol(‘My symbol’);
“your symbol is “ + sym // TypeError: can’t convert symbol to string
//——————————————————-
//Symbol 值可以显式转为字符串 let sym = Symbol(‘My symbol’);
String(sym) // ‘Symbol(My symbol)’ sym.toString() // ‘Symbol(My symbol)’
//———————————————————-
//Symbol 值也可以转为布尔值,但是不能转为数值 let sym = Symbol(); Boolean(sym) // true !sym // false
if (sym) { // … }
Number(sym) // TypeError sym + 2 // TypeError
<a name="Um2g6"></a>
#### description 描述
```javascript
const sym = Symbol('foo');
sym.description // "foo"
作为属性名Symbol
let mySymbol = Symbol();
// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
let a = {
[mySymbol]: 'Hello!' //在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。
};
// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上写法都得到同样结果
a[mySymbol] // "Hello!"
//--------------------------------------------------------------
const log = {};
log.levels = {
DEBUG: Symbol('debug'),
INFO: Symbol('info'),
WARN: Symbol('warn')
};
console.log(log.levels.DEBUG.description, 'debug message');
console.log(log.levels.INFO.description, 'info message');
//debug debug message
//info info message
Symbol 值作为属性名时,该属性是公有属性不是私有属性,可以在类的外部访问。但是不会出现在 for…in 、 for…of 的循环中,也不会被 Object.keys() 、 Object.getOwnPropertyNames() 返回。如果要读取到一个对象的 Symbol 属性,可以通过 Object.getOwnPropertySymbols() 和 Reflect.ownKeys() 取到。
const obj = {}
let a = Symbol('a');
let b = Symbol('b');
obj[a] = 'hello'
obj[b] = 'world'
const objectSymbols = Object.getOwnPropertySymbols(obj)
console.log(objectSymbols)
//[ Symbol(a), Symbol(b) ]
同一个Symbol值
重新使用同一个 Symbol 值,Symbol.for()方法可以做到这一点
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
s1 === s2 // true
Symbol.for()与Symbol()这两种写法,都会生成新的 Symbol。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。
for …of
全新的遍历方式 for…of 遍历 可以遍历所有所有数据结构。
一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for…of循环遍历它的成员。也就是说,for…of循环内部调用的是数据结构的Symbol.iterator方法。
for…of循环可以使用的范围包括:
- 数组
- Set
- Map 结构
- 某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、 Generator 对象
- 字符串。
```javascript
const arr = [1,2,3,4]
for (const i of arr) {
console.log(i)
if(i>3) {
} } //—————————————————————————————————— const arr = [‘a’,’b’,’c’, ‘d’] for(let i in arr) { console.log(i) } //0 1 2 3 4 for(let i of arr) { console.log(i) } //a b c d //JavaScript 原有的for…in循环,只能获得对象的键名,不能直接获取键值。ES6 提供for…of循环,允许遍历获得键值。break //可以用break随时终止循环 优于forEach
//for…of循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。这一点跟for…in循环也不一样。 let arr1 = [3, 5, 7]; arr1.foo = ‘hello’;
for (let i in arr1) { console.log(i); // “0”, “1”, “2”, “foo” }
for (let i of arr1) { console.log(i); // “3”, “5”, “7” } //———————————————————————————————————- // 可以遍历map set对象 const s = new Set([‘foo’,’bar’]) for (const item of s) { console.log(s) } // 跟数组差不多也是拿到的每一项
const m = new Map() m.set(‘foo’,’124’) m.set(‘foo1’,’4456’) for (const [value, key] of m) { console.log(value, key) }
下面我们具体说说iterator接口。
<a name="rwbhy"></a>
### iterator 迭代器(遍历器)
<a name="o5zQD"></a>
#### 前言
原来表示“集合”的数据结构,数组(Array)和对象(Object),ES6 又添加了Map和Set。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是Map,Map的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。
<a name="b9PEs"></a>
#### 概念
遍历器(Iterator)就是这样一种机制。**它是一种接口,为各种不同的数据结构提供统一的访问机制**。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作。<br />其实Iterator 接口主要供for...of使用(**一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历**)。<br />我们来看个例子
```javascript
// iterator 迭代器
const set = new Set(['foo','bar','baz'])
console.log(set)
const iterator = set[Symbol.iterator]()
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
//Set { 'foo', 'bar', 'baz' }
//{ value: 'foo', done: false }
//{ value: 'bar', done: false }
//{ value: 'baz', done: false }
//{ value: undefined, done: true }
//{ value: undefined, done: true }
//所以我们能看出来Iterator 的遍历过程
从代码里我们能看出来Iterator的遍历过程
- 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
- 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
- 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
- 不断调用指针对象的next方法,直到它指向数据结构的结束位置。
返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。
//模拟实现Iterator
function makeIterator(array) {
var nextIndex = 0
return function() {
return nextIndex< array.length?
{value: array[nextIndex++],done:false}:
{value:undefined,done:true}
}
}
var it makeIterator(['a', 'b'])
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
所以只要数据有Symbol.iterator这个属性就可以
//现在是for of 无法循环普通对象 是因为它内部没有实现iterable 接口
const obj = {}
for( const itme of obj) {
console.log(item)
}
// TypeError: obj is not iterable
//假如我们内部实现iterable接口 那这个对象就可以被for of 循环
const obj = {
store:['foo','bar','baz'],
[Symbol.iterator] : function() {
let index = 0
const self = this
return { //从内部再返回一个迭代器
next: function() {
const result = {
value: self.store[index],
done: index>=self.store.length //表示迭代有没有结束
}
index ++
return result
}
}
}
}
for( const item of obj) {
console.log(item)
}
原生具备 Iterator 接口的数据结构如下。
-array
-map
-set
-map
-string
-TypedArray
-函数的arguments对象
-nodeList 对象
generator
基本概念:
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
特征:
- function关键字与函数名之间有一个星号
- 函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)
看一个例子
//基本使用
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator(); //调用
//-------------------------------------------
hw.next() //调用遍历器对象的next方法,使得指针移向下一个状态
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
- 上面代码定义了一个 Generator 函数helloWorldGenerator。
- 调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)
- 每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。或者说(yield表暂停执行,next恢复执行)
- 每次调用遍历器对象的next方法,返回的value和done。value是yield表达式后面那个表达式的值;done表示是否遍历结束。
感觉像是利用Iterator实现对代码的分段执行能暂停能再开始。
使用案例
// generator 案例
//案例1 : 实现一个发号器
function *createIdMaker() {
let id = 1;
while(true) {
yield id ++
}
}
const idMaker = createIdMaker()
console.log(idMaker.next().value)
console.log(idMaker.next().value)
console.log(idMaker.next().value)
console.log(idMaker.next().value)
console.log("---------------------------")
//案例二 : 实现iterator 迭代器
const todos = {
left:['吃饭','睡觉','打豆豆'],
learn: ['语文','数学','英语'],
work: ['喝茶'],
// 实现迭代器
[Symbol.iterator]: function *() { //因为它本身就返回Iterator迭代器
const arr = [].concat(this.left, this.learn, this.work)
for (const item of arr) {
yield item
}
}
}
for (const item of todos) {
console.log(item)
}
// 1
// 2
// 3
// 4
// ---------------------------
// 吃饭
// 睡觉
// 打豆豆
// 语文
// 数学
// 英语
// 喝茶
再再具体的功能再查阅相关文档的使用吧。
promise已有相关文档介绍,下面介绍下async函数
async函数
含义
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。
async 函数是什么?一句话,它就是 Generator 函数的语法糖。
看个例子:
const fs = require('fs');
// 依次读取两个文件
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
//generator写法
const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
//---------------------------------------------------------
//async写法
const asyncGen = async function() {
const f1 = await readFile('/etc/fastab')
const f2 = await readFile('etc/shells')
console.log(f1.toString());
console.log(f2.toString());
}
一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await。
async对generator改进具体是:
- 内置执行器。
Generator 函数的执行必须靠执行器执行得用next() 方法,而async自带执行器,执行就一行就可以
asyncGen()
- 更好的语义。
async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
- 更广的适用性
yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值
- 返回值是 Promise
async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。
进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。
基本用法
指定多少毫秒后输出一个值
function timeout(ms) {
return new Promise((resolve)=> {
setTimeout(resolve, ms)
})
}
async function asyncPrint(value, ms) {
await timeout(ms)
console.log(value)
}
asyncPrint('hello world', 1000)
上面代码指定 1000 毫秒以后,输出hello world。
在我们实际使用上,因为await后面的promise对象,有可能失败,最好把await放到try..catch里
function timeout(ms) {
return new Promise((resolve)=> {
setTimeout(resolve, ms)
})
}
async function asyncPrint(value, ms) {
try {
await timeout(ms)
console.log(value)
}catch(err) {
console.log(err)
}
}
asyncPrint('hello world', 1000)
更多详细的使用具体使用的时候再看下。
实现异步的方式 generator promise async,都说async是解决异步的终极方案,可以多多使用。加油下一章我们介绍ES2016和ES2017新增的东西。