ECMAScript 6 概览
--摘自 阮一峰老师 ECMAScript 6--
let 与const 命令
一.块级作用域:注(
-
-
允许在块级作用域内声明函数。
-
函数声明类似于
var
,即会提升到全局作用域或函数作用域的头部。 -
同时,函数声明还会提升到所在的块级作用域的头部。
-
注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作let
处理。
)
二.不存在变量提升
三.暂时性死区(如果区块中存在let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错)
四.let 不允许同一作用域内,重复声明
const 常量
一.值不能改变,声明变量后须立即初始化。
二.与let 相同,只在声明所在的块及作用域内有效。const
命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。不能重复声明
三.const ,值不能改变,不能指向其他值
四.冻结对象 Object.freeze({});
ES6六种声明变量的方法:var,function,let,const, import,class
顶层对象的属性与全局变量
一.let,const,class 命令声明的全局变量 不属于顶层的属性 let i; window.i //undefined
二.全局环境中,Node模块和ES6模块中,this
返回的是当前模块。
变量的解构与赋值
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
let [a,b,c] = [1,2,3]; let {bar,foo} = {foo:"aaa",bar:"bbb"};let{foo:bar} = {bar:"bbb"};
字符串的操作
一.字符的Unicode表示法
二 .codePointAt();方法会正确返回32位的UTF-16字符的码点
三.字符串遍历:优点是可以识别大于0xFFFF
的码点,传统的for
循环无法识别这样的码点。
for (let codePoint of 'foo') {
console.log(codePoint)
}
// f,o,o
四.ar();该方法不能识别码点大于
0xFFFF
的字符。
五.用来确定一个字符串是否包含在另一个字符串中。ES6又提供了三种新方法。
-
-
includes():返回布尔值,表示是否找到了参数字符串。
-
startsWith():返回布尔值,表示参数字符串是否在源字符串的头部。
-
endsWith():返回布尔值,表示参数字符串是否在源字符串的尾部。
-
六. repeat();返回一个新的字符串,表示将原字符串重复n次。x.repeat(3);//"x x x"
七.padStart(),padEnd();字符串不全长度如果字符串不够指定的长度就会在 头部或者尾部补全
八.模板字符串:用反引号(`)标识$('#list').html(`
<ul>
<li>first</li>
<li>second</li>
</ul>
`);
正则的拓展
如果RegExp构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。
new RegExp(/abc/ig, 'i').flags
字符串对象共有4个方法,可以使用正则表达式:match()
、replace()
、search()
和split()
。
ES6将这4个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上。
-
String.prototype.match
调用RegExp.prototype[Symbol.match]
-
String.prototype.replace
调用RegExp.prototype[Symbol.replace]
-
String.prototype.search
调用RegExp.prototype[Symbol.search]
-
String.prototype.split
调用RegExp.prototype[Symbol.split]
一.修饰符
ES6对正则表达式添加了
ES6还为正则表达式添加了u
修饰符,含义为“Unicode模式”,用来正确处理大于\uFFFF
的Unicode字符。也就是说,会正确处理四个字节的UTF-16编码。y
修饰符,叫做“粘连”(sticky)修饰符。y
修饰符的作用与g
修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g
修饰符只要剩余位置中存在匹配就可,而y
修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义ES6还为正则表达式添加了S修饰符正则表达式中,点(.
)是一个特殊字符,代表任意的单个字符,但是行终止符(line terminator character)除外。
二.后行断言:V8 引擎4.9版已经支持,Chrome 浏览器49版打开”experimental JavaScript features“开关(地址栏键入about:flags
),就可以使用这项功能。
三.Unicode属性类:一种新的类的写法\p{...}
和\P{...}
,允许正则表达式匹配符合Unicode某种属性的所有字符
数值的拓展
二进制与八进制表示法
ES6 提供了二进制和八进制数值的新的写法,分别用前缀0b
(或0B
)和0o
(或0O
)表示。0o767 === 503
ES6在Number对象上,新提供了Number.isFinite()
和Number.isNaN()
两个方法。Number.isFinite()
用来检查一个数值是否为有限的(finite)
Number.parseInt(), Number.parseFloat()
Number.isInteger()
安全整数和Number.isSafeInteger()表示这个范围的上下限
Math对象的扩展
ES6在Math对象上新增了17个与数学相关的方法。所有这些方法都是静态方法,只能在Math对象上调用。Math.trunc(),Math.cbrt(),Math.clz32(),Math.imul(),Math.fround()...
数组的拓展Array.from
方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括ES6新增的数据结构Set和Map)
实际应用中,常见的类似数组的对象是DOM操作返回的NodeList集合,以及函数内部的arguments
对象。Array.from
都可以将它们转为真正的数组。
// NodeList对象
let ps = document.querySelectorAll('p');
Array.from(ps).forEach(function (p) {
console.log(p);
});
// arguments对象
function foo() {
var args = Array.from(arguments);
// ...
}
Array.of()
Array.of
方法用于将一组值,转换为数组。
Array.of(3, 11, 8) // [3,11,8]
数组实例的copyWithin()
数组实例的copyWithin
方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。
数组实例的find()和findIndex()
数组实例的find
方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true
的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined
。
entries(),keys()和values()
includes()
fill()
---函数的拓展---
函数参数可以设置默认值 function Point(x = 10,y = 0){ this.x = x;this.y = y;} var p = new Point(); 但是不能用let const 再次声明 否则会报错。
也不能用重复的参数名;
函数参数的个数,设定默认值的参数不计入length个数(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
如果设置了默认值的参数不是尾参数,那么
length
属性也不再计入后面的参数了。
作用域:一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域
rest参数:(形式为“...变量名”),用于获取函数的多余参数,这样就不需要使用arguments
对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
rest参数中的变量代表了一个数组 数组中特有的方法可以用于这个变量, 函数的length属性不包括 rest参数
拓展运算符。拓展运算符... 将一个数组转换为用逗号分隔的参数序列。该运算符主要用于函数的调用。
拓展运算符可以展开数组所以不需要apply方法也可以. 如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错
--箭头函数--
var f = v => v; / var f = function(v){return v};
如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
var f = () =>v; var sum =(num1,num2) => num1+num2;
如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return
语句返回。
var sum =(num1 ,num2)=>{return num1+num2};
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号
var getTempItem = id =>({id:id,name:'temp'});
箭头函数可以与变量解构结合使用。
箭头函数有几个使用注意点。
(1)函数体内的this
对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new
命令,否则会抛出一个错误。
(3)不可以使用arguments
对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。
(4)不可以使用yield
命令,因此箭头函数不能用作Generator函数。
箭头函数可以让this指向固定化
this
指向的固定化,并不是因为箭头函数内部有绑定this
的机制,实际原因是箭头函数根本没有自己的this
,导致内部的this
就是外层代码块的this
。正 是因为它没有this
,所以也就不能用作构造函数。
由于箭头函数没有自己的this
,所以当然也就不能用call()
、apply()
、bind()
这些方法去改变this
的指向。箭头函数没有自己的this
,所以bind
方法 无效,内部的this
指向外部的this
。
--尾调用--:某一个函数的最后一步是调用另一个函数:另一个函数不会不在引用包含函数的值(类似于 栈执行现场 的一种 垃圾回收机制吧)
--尾递归--:函数调用自身称为递归,尾函数调用自身称为 尾递归。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
function factorial(n,total){
if(n === 1) return total;
return factorial(n -1,n* total);
}
factorial(5,1);//120; 复杂度O(1);
---对象的拓展---
var foo = 'bar';
var baz = {foo}; //{foo:'bar'}
ES6允许在对象中直接写变量
function(x,y){return:x,y} ;// function(x,y){ return {x:x,y:y}};
方法也可以简写
var o = {method(){
return 'hello';
}}
等同于
var o = {
method:function(){
return 'hello!';
}
}
示例;
var birth = '2000/01/01';
var Person = {
name:'张',
birth,
hello(){console.log('我的名字是',this.name);}
};
示例;
function getPoint(){
var x = 1;
var y =10;
return {x,y};
}
getPoint();//{x:1,y:10}
--属性名表达式
obj['a'+'b'] = 123; // var obj = {foo:true,abc:123};
let proKey = 'foo';
let obj = {
[proKey]:true,
['a'+'bc']:123
};
定义方法名
let obj = {
['h'+'hello'](){
return 'hi';
}
};
obj.hello();// hi
--属性名表达式 如果是一个对象,默认情况下就会将对象 转换成字符串[object object]
const keyA = {a:1};
const keyB = {b:2};
const myObject = {
[keyA]:'value1',
[keyB]:'value2'
};
myObject //object {[object object]:'value2'};
--方法的name属性--
函数的name 属性,返回函数名,对象方法也是函数,因此也有name属性
--Object.is();
ES5比较两个值是否相等,只有两个运算符:相等运算符(==
)和严格相等运算符(===
)。它们都有缺点,前者会自动转换数据类型,后者的NaN
不等于自身,以及+0
等于-0
。JavaScript缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。
ES6提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is
就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致
Object.assign();用于对象合并,将源对象source 的所有可枚举属性,赋值到目标对象
Object.assign
方法的第一个参数是目标对象,后面的参数都是源对象
如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性
ES6一共有5种方法可以遍历对象的属性。
(1)for...in
for...in
循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)。
(2)Object.keys(obj)
Object.keys
返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)。
(3)Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames
返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)。
(4)Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols
返回一个数组,包含对象自身的所有Symbol属性。
(5)Reflect.ownKeys(obj)
Reflect.ownKeys
返回一个数组,包含对象自身的所有属性,不管是属性名是Symbol或字符串,也不管是否可枚举。
以上的5种方法遍历对象的属性,都遵守同样的属性遍历的次序规则。
- 首先遍历所有属性名为数值的属性,按照数字排序。
- 其次遍历所有属性名为字符串的属性,按照生成时间排序。
- 最后遍历所有属性名为Symbol值的属性,按照生成时间排序。
null 传导符
如果读取对象内部的某个属性,一般需要版判断该对像是否存在,为了解决这样的麻烦,因此现在有个提案,引入的null传导符 ?.
const fristName = message?.body?.user?.firstName || 'default';
上面代码有三个?.
运算符,只要其中一个返回null
或undefined
,就不再往下运算,而是返回undefined
。
“Null 传导运算符”有四种用法。
obj?.prop
// 读取对象属性obj?.[expr]
// 同上func?.(...args)
// 函数或对象方法的调用new C?.(...args)
// 构造函数的调用
传导运算符之所以写成obj?.prop
,而不是obj?prop
,是为了方便编译器能够区分三元运算符?:
(比如obj?prop:123
)。
--Symbol--
ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。它是JavaScript语言的第七种数据类型,前六种是:Undefined、Null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。
let s = Symbol();
typeof s //'symbol'
--Set和Map数据结构--
Set,类似于数组,但是成员值都是唯一的,没有重复的值。Set 本身就是一个构造函数,用来生成数据结构
console.log([...new Set([1,1,2,3,4,5])]) ;//1 2 3 4 5
Array.from()可以将数Set结构转化为数组结构
var items = new Set([1,2,3,4]);
var array = Array.form(items);
function dedup(array){
return Array.from(new Set(array));
}
dedupe([1,2,3,3,4,5]); //[1,2,3,4,5];
--WeakSet--
WeakSet结构与Set类似,也是不重复的值的集合。但是,它与Set有两个区别。
首先,WeakSet的成员只能是对象,而不能是其他类型的值。
其次,WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于WeakSet之中。这个特点意味着,无法引用WeakSet的成员,因此WeakSet是不可遍历的。
WeakSet是一个构造函数,可以使用new
命令,创建WeakSet数据结构
var ws = new WeakSet();
WeakSet没有size
属性,没有办法遍历它的成员。
WeakSet不能遍历,是因为成员都是弱引用,随时可能消失,遍历机制无法保证成员的存在,很可能刚刚遍历结束,成员就取不到了。WeakSet的一个用处,是储存DOM节点,而不用担心这些节点从文档移除时,会引发内存泄漏
ES6提供了Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object结构提供了“字符串—值”的对应,Map结构提供了“值—值”的对应,是一种更完善的Hash结构实现。如果你需要“键值对”的数据结构,Map比Object更合适。
Map 构造函数接受数组作为参数,
var items = [
['name','张三'],
['title','Auther']
];
var map = new Map();
items.forEach(([key,value])=>map.set(key,value));
如果对同一个键多次赋值,后面的值覆盖前面的值
let map = new Map();
map.set(1,'aaa').set(1,'bbb');
map.get(1); // bbb
--WeekMap--WeakMap
结构与Map
结构基本类似,唯一的区别是它只接受对象作为键名(null
除外),不接受其他类型的值作为键名,而且键名所指向的对象,不计入垃圾回收机制。WeakMap
的设计目的在于,键名是对象的弱引用(垃圾回收机制不将该引用考虑在内),所以其所对应的对象可能会被自动回收。当对象被回收后,WeakMap
自动移除对应的键值对。典型应用是,一个对应DOM元素的WeakMap
结构,当某个DOM元素被清除,其所对应的WeakMap
记录就会自动被移除。基本上,WeakMap
的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap
结构有助于防止内存泄漏WeakMap与Map在API上的区别主要是两个,一是没有遍历操作(即没有
key()
、values()
和entries()
方法),也没有size
属性;二是无法清空,即不支持clear
方法。这与WeakMap
的键不被计入引用、被垃圾回收机制忽略有关。因此,WeakMap
只有四个方法可用:get()
、set()
、has()
、delete()
。
---Proxy---
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程
var proxy = new Proxy(target,handler);
Proxy
接受两个参数。第一个参数是所要代理的目标对象,第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作
要使得Proxy
起作用,必须针对Proxy
实例(上例是proxy
对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。
---Reflect---
Reflect
对象与Proxy
对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect
对象的设计目的有这样几个
大部分与Object
对象的同名方法的作用都是相同的,而且它与Proxy
对象的方法是一一对应的。
---Promise---
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6将其写进了语言标准,统一了用法,原生提供了Promise
对象。
所谓Promise
,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
Promise
对象有以下两个特点。
(1)对象的状态不受外界影响。Promise
对象代表一个异步操作,有三种状态:Pending
(进行中)、Resolved
(已完成,又称 Fulfilled)和Rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise
这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise
对象的状态改变,只有两种可能:从Pending
变为Resolved
和从Pending
变为Rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对Promise
对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
有了Promise
对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise
对象提供统一的接口,使得控制异步操作更加容易。
Promise
也有一些缺点。首先,无法取消Promise
,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise
内部抛出的错误,不会反应到外部。第三,当处于Pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
如果某些事件不断地反复发生,一般来说,使用 stream 模式是比部署Promise
更好的选择。
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。它们是两个函数,由JavaScript引擎提供,不用自己部署。
promise的错误有冒泡的性质,会一直向后传递,知道被捕获位置,也就是说,错误总是会被下一个catch语句捕获。
一般来说,不要在then方法里面定义reject状态的幻术(即then的第二个参数),总是使用catch方法
promise all() ;promise race() ;pormise.prototype.catch();promise.resolve();
---Iterator---(遍历器)的概念
遍历器(Iterator),就是这样一种机制。他是一种接口,为各种不同的数据结构提供统一的接口机制,来处理不同的数据结构。
一,一是为各种数据结构,提供一个统一的简便的接口;二使得结构的成员能够按某种次序排列三,s6,创造了一种新的遍历命令 for ...of ,Iterator接口主要提供for...of消费。
Iteration的遍历过程是这样的
1.创建一个指针对象,指向当前数据结构的起始位置,也就是说,遍历器最想的本质上,就是一个指针对象
2.第一次调用指针对象的next方法,就可以将指针指向数组结构的第一个成员。
3.第二次调用指针对象的 next,指针就指向数据结构的第二个成员。
4.不断调用指针对象的next方法,知道它指向数据结构的结束为止。
每一次调用next方法,都会返回数据结构的当前成员信息,具体来说,就是返回一个value和done两个属性的对象,其中value属性是当前成员的值,done属性就是个布尔值,表示遍历是否结束。
var it = makeIterator(['a',''b]);
it.next()
ES6规定,默认的Iterator接口部署在数据结构的 Symbol.iterator属性,或者说,一个数据结构
只要具有symbol.iterator属性,就可以认为是可遍历的(iterable)Symbol.iterator属性本身就是一个函数
就是当前数据结构默认的遍历生成器函数,执行这个函数就会返回一个遍历器。治愈属性名,它是
一个表达式,返回 symbol对象的iterator属性,这是一个预定义好的类型为symbol的特殊值,所以要
放在方括号内
const obj = {
[Symbol.iterator]:function(){
next:function(){
return{ value:1,done:true}
}
}
}可以将任何部署了iterator接口的数据结构转换为数组,也就是说
只要某个数据结构部署了iterator接口,就可以对它使用拓展运算符将其转换为数组
let arr = [...iterable];
字符串的iterator接口
字符串也是一个类数组对象,也具有iterator接口
var someString = 'hi';
typeof someString[Symbol.iterator]
var iterator = someString[Symbol.iterator]();
iterator.next()
iterator.next()
iterator.next() //{value:undefined,done:true}
由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。
- for...of
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()(比如
new Map([['a',1],['b',2]])
) - Promise.all()
- Promise.race()
for in 循环智能读取键名,for..of 循环 读取键值,如果要通过for..of
let arr = [3,5,7];
arr.foo = 'hello';
for (let i in arr){
console.log(i);// '0','1','2','foo'
}
for (let i of arr){
console.log(i); // '3','5','7'
}
set 和 map 结构也具有 iterator接口
---for of 循环---
并不是所有 类似数组的对象都具有iterator接口,一个简便的解决方法
就是用Array.from 方法将其转换成数组
let arrayLike = {
lenth:2,0:'a',1:'b'
}
for(let x of arrayLike){
console.log(x);// 报错
}
//正确
for(let x of Array.form(arrayLike)){
console.log(x);
}
--Generator-- 函数的语法
Generator 函数 是ES6提供的一种异步编程解决方案,语法行为与传统的函数完全不同
Generator 函数有多重理解的角度,从语法上,可以把它理解成,Generator函数是一个
状态机,封装了多个内部状态
执行Generator 函数返回一个遍历器对象,也就是说,Generator函数除了状态
还是一个遍历器对象生成的函数,返回遍历器对象,可以一次便利Generator函数内部
的每一个状态
形式上Genertor 函数是一个普通的函数,但是有两个特征,一个是,function关键字
与函数名之间有一个星号,
二是,函数体内部使用yield语句,定义不同的内部状态
function* helloWorldGenerator(){
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
上面代码定义个一个 Generator函数 ,它内部有两个yield语句,即 改函数有三个状态
结束执行。
Generator函数的调用方法与普通函数一样也是在函数后面加一对圆括号
不同的是调用 函数后 该函数并不执行,返回的也不是函数运行的结果,而是一个
指向内部状态的指针对象,也就是上一张介绍的Iterator Object
下一步必须调用遍历器对象的next方法,是的指针移向下一个状态,也就是说,每次
调用next方法,内部指针就从函数同步或者上一次停下来的地方开始执行
直到遇到下一个yield语句 或者return语句 为止。 Generator函数是分段执行的,
yield 语句是展厅执行的编辑,而next方法可以恢复执行
--yield--
由于Generator函数返回的遍历器对象,只用调用next方法才会遍历下一个内部状态,所以其实
提供了一中国可以暂停的函数,yield语句就是暂停的标志
遍历器对象的next方法的运行逻辑如下
1.遇到yield语句 就暂停执行后面的操作,并紧跟在yield后面那个表达式的值作为返回的
对象的value属性的值。
2.下一次调用next方法时候,就继续往下执行直到遇到下一个yield语句。
3.如果没有遇到新的yield语句,就一直运行到函数结束。直到return语句为止,并将return
语句后面的表达式的值,作为返回对象的value值。
4.如果该函数没有return语句,则返回的对象对象的value属性值为undefined。
yield 语句如果用于表达式中必须放在圆括号里面
function* demo(){
console.log('hello'+ yield);//syntaxError
console.log('hello'+ yield 123);//syntaxError
console.log('hello'+ (yield);//ok
console.log('hello'+ (yield 123));//ok
}
yield.语句用作函数参数 或者放在赋值表达式的右边,可以不加括号
function* demo(){
foo(yield 'a' , yield 'b'); //ok
let input = yield; //ok
}
与 Iterator接口的关系
任意一个对象的symbol.iterator方法 等于该对象的遍历器生成函数,
调用该函数就会返回对象的一个遍历器对象
Generator函数执行后,返回一个遍历器对象,该对象本身也具有Symbol.iterator属性执行后返回
自身
var function* (){
// some code
}
var g = gen();
g[Symbol.iteraor]() === g;
//true
---next方法的参数---
yield句本身没有返回值,或者说,总是返回undefined。next方法可以带一个参数
该参数机会被当做上一个yield语句的返回值。
这个功能有个很重要的意义。Generator函数从暂停状态到回复运行,他的上下文状态
是不变的,通过next方法的参数,就有办法在Generator函数开始运行之后,继续想函数
体内注入值。也就是说,可以在Generator函数运行的不同阶段,从外部向内部注入不同的
值,从而调整函数的行为
Generator.prototype.throw();
函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误
然后Generator函数体内捕获
Generator.prototype.return();
函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且
终结Generator函数
function* gen(){
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next()
g.return('foo'); //{value:'foo',done:ture}
g.next() //{value:undfined}
---yield*--语句
如果Generator函数内部,调用另一个个Generator函数,默认情况下是没有效果的
从语法角度看,如果 yield 命令后面跟的是一个遍历器对象,需要在yield命令后面加上
星号,表明它返回的是一个遍历器对象,这被称作为yield语句
如果yield* 后面跟着一个数组,由于数组原生支持遍历器,因此会遍历数组成员
function* gen(){
yield* ['a','b','c'];
}
gen.next()
yield* 命令可以很方便的取出嵌套数组的所有成员
Generator函数总是返回一个遍历器,ES6规定这个遍历器是Generator函数的实例
也继承了Generator函数的prototype对象上的方法
function* g(){}
g.prototype.hello = function(){
return 'hi!';
}
let obj = g();
obj instaceof g //true
obj.hello();
Generator.js 不能跟函数命令一起使用,会报错
function* F(){
yield this.y = 2;
yield this.x = 3;
}
try{
new F()
}catch (e){
console.log(`这个错误是 ${e}`);// typeError: F is not constructor
}
Generator 与携程
携程 是一种程序的运行方式,可以理解成协作的线程 或者 协作的函数
协成既可以用单线程实现,也可以用多线程实现,前者是一种特殊的子例程或者是一种
特殊的线程
协成与子例程的差异
传统的子例程 曹勇 堆栈式 “后进后出”的执行方式,只用当调用的子函数完全执行完毕
才会结束执行的父函数,
协程不同,多个线程(单线程的情况下,即是多个函数)可以并行执行,但是只有
一个线程或者函数 处于运行状态 其他线程或者函数 都处于暂停状态,
线程 或者函数 之间可以交换执行权,也就是说,一个线程或者函数,执行到一般,可以
暂停执行,将执行权交给另一个线程或者函数,等到稍后回收执行权的时候,再回复执行,这种可以
这种可以并行的执行交换执行权的线程,或者函数 就是 协成
从现实上看,在内存中,子例程只是用一个栈stack 而协成就是同事存在多个栈但是
只用一个栈是在云zing状态的,也就是说,协成是以占用内存为代价,实现多任务的并行
--协程与普通线程的差异
不难看出 协成适合用于多任务运行的环境,在这个意义上,它与普通的线程很相似
都有自己的执行上下文,可以分享全局变量,他们的不同之处在于,统一时间可以有多个
线程处理运行状态,但是运行的协成只能有一个,其他协成都处于暂停
状态
普通的线程是抢先式的。到底哪个线程优先得到资源必须有运行环境决定,但是协成是
合作式的执行权由协成自己分配
Gnerator函数的异步应用
所谓异步 简单说 就是一个任务不是连续完成的,可以理解为被
人为的分成了两段,先执行第一段,然后执行其他任务,等做好了
准备再回头执行第二段。
回调函数本身并没有什么问题,他的问题出现在多个回调函数嵌套,
多个一步操作形成了强耦合,只要有一个操作需要修改,他的上层
回调函数和下层回调函数 都会跟着修改,这样就称为回调地狱
---async 函数
ES2017 标准 引入了 async 函数,使得异步函数操作更加方便
async 函数身什么?一句话,他就是Generator函数的语法糖
var async ReadFile = async function (){
var f1 = await readFile('/ted/fs');
var f2 = await readeFile('/etc/shells');
console.log(f1.tostring());
}
async 函数 对Genrator函数的改进 体现在一下四点
1.内置执行器
Generator函数的执行必须考执行器,所以猜啊有了co模块。
而 async 函数自带执行器,也就是说,async函数的执行与普通函数一模一样,只要
一行
var result=asyncReadFile();
上面代码调用了asyncFile函数,然后 他就会自动执行,输出最后的结果,
者完全不行Generator函数需要调用next方法,或者co 模块,才能真正执行,得到最后的
结果
2.更好的语义
async和await 比起星号和 yield 语义更加清楚了,async 表示函数里有异步操作
await 表示紧跟在后面的表达式需要等待结果
3.更广的适应性
co模块的约定,yield命令后面只能是 Thunk 函数或者是promise对象, 而
async函数的await命令后面,可以是promise对象和原始类型的值字符串,但这是
等同于同步操作。
返回值是promise。
async函数的返回值是promise对象,这比generator 函数的返回值是Iterator
对象方便多了。你可以用then方法指定下一步操作
进一步说,async 函数完全可以看做是多个异步操作,包装成的promise对象
从而await命令 就是内部then命令的语法糖。
---await---
正常情况下 await命令后面是一个promise 对象,如果不是,会被转换成一个立即resolve的
promise 对象
async function f(){
return await 123;
}
f().then(v=> console.log(v))
// 123
如果 await 语句后面的promise变为reject 那么 整个函数都会中断执行。
---async ---函数的实现原理
async 函数实现的原理,就是将 Generator 函数和自动执行器,包装在一个函数里
async function fn(args){
//...
}
//等同于
function fn(args){
return apawn(function * (){
//...
})
}
//所有的asyn函数都可以写成第二种形式,其中的spawn 函数就是自动执行器
下面给出 spawn 函数的实现,基本上就是自动执行器的翻版
---calss---
/*
javascript 语言的传统方法是通过构造函数,定义并生成新的对象,
*/
// function Point(x,y){
// this.x = x;
// this.y = y;
// }
// Point.prototype.toString = function(){
// return '('+ this.x +','+ this,y + ')';
// };
// var p = new Point(1,2);
/*
上面这种写法,跟传统的面向对象语言(c++ 和 Java)差异很大,很容易让
新学习这门语言的程序员感到困惑
es6 提供了更接近传统语言的写法,引入了calss 类的概念,作为对象的模板
通过class关键字,可以定义类,基本上,ES6 class 可以看做是一个语法糖,
他的绝大部分功能,ES5 都可以做到,新的 class 写法 值是让对象原型的写法
更加清晰,更加像 面向对象编程的语法而已。
*/
//定义类
class Point{
constructor(x,y){
this.x = x;
this.y = y;
}
toString(){
return '('+ this.x +','+this.y+')';
}
}
/*
以上代码定义了一个类,可以看到里面有一个constructor方法,这就是构造方法,
而this关键字,则代表实例对象,也就是说,ES5的构造函数Point,对应ES6的 Point
类的构造方法
Point 类除了构造方法,还定义了一个toString方法,注意,定义类的方法的时候,前面
不要加function关键字,直接把函数定义放进去就可以了,另外,方法之间不需要逗号分隔符
加了就会报错!!!
ES6的类完全可以看做是构造函数的另一种写法。
*/
console.log(typeof Point ); //function
console.log(Point === Point.prototype.constructor); //true
// 上面代码表明,类的数据类型就是函数,类本身就指向构造函数,使用的时候也就对类使用new命令,跟构造函数的用法完全一致
class Bar{
doStuff(){
console.log('hello Class ES6 stuff');
}
}
var b = new Bar();
b.doStuff();
// 构造函数的Prototype属性,在ES6的 类上面继续存在,事实上,类的所有方法都定义子在类的
// prototype属性上面
class Point2{
constructor(){
//....
}
toString(){
//..
}
toValue(){
}
}
//等同于
Point2.prototype = {
toString(){},
toValue(){}
};
// prototype 对象的 construct 属性,直接指向类的本身,这与ES5的行为是一一致的
// 另外 ,类的内部所有定义的方法,都是不可枚举的
class Point3 {
constructor(x,y){
}
toString(){
}
}
console.log('所有类 内部定义的方法都是不可枚举的'+ Object.keys(Point3.prototype));
console.log('方法在内部'+ Object.getOwnPropertyNames(Point3.prototype));
// 类的属性名 可以采用表达式
let methodName = 'getArea';
class Square{
constructor(length){
//
}
[methodName](){
//....
}
}
//constructor方法是类的默认值防范 通过new 命令生成对象实例时候,自动调用该方法
/*
一个类必须有constructor方法,如果没有显示的定义,一个空的constructor 方法就会被
默认添加
constructor方法 默认返回实例对象 即this,完全可以指定返回另一个对象。
*/
class Foo{
constructor(){
return Object.create(null);
}
}
console.log(new Foo() instanceof Foo )//false
/*
上面代码中,constructor函数 返回一个全新的对象,结果导致实例对象不是
Foo类的实例
类的构造函数,不使用new 是没有办法调用的,就会报错,他跟普通构造函数的一个主要
区别,后者不用new 也可以执行
*/
// 类的实例对象
/*
生成类的实例对象的写法 ,与ES5完全一样,也就是使用new命令,如果王佳加上new,
像函数那样调用 class ,将会报错
*/
// var point = Point(2,3); 报错
// var point = new Point(2,3); 正确
/*
与ES5一样实例的属性除非定义在其本身,(即定义在this对象上),否则都是定义在原型上
(即定义在class上
*/
class Point4{
constructor(x,y){
this.x = x;
this.y = y;
}
toString(){
return this.x + this.y;
}
}
var point4 = new Point4(2,4);
console.log(point4.toString()); //6
console.log(point4.hasOwnProperty('x'));//true
console.log(point4.hasOwnProperty('y'));// true
console.log(point4.hasOwnProperty('toString'))//false
console.log(point4.__proto__.hasOwnProperty('toString')) //true
//与ES5一样,类的所有实例 共享一个原型对象。
var point4_1 = new Point4(1,2);
var point4_2 = new Point4(2,3);
console.log(point4_1.__proto__ == point4_2.__proto__); //true
//这也就意味着,可以通过__proto__属性 为Class 添加方法
var p1 = new Point4(1,2);
var p2 = new Point4(2,3);
p1.__proto__.printName = function (){ return '宋仁泽'};
var p3 = new Point4(2,2);
console.log(`通过__proto__,属性(prototype的属性) 添加的方法 能被 所有实例对象访问 ${p2.printName() + p3.printName()}`);
/*
上面代码中 p1 的原型上添加了一个printName方法由于p1 的原型对象就是p2 的原型
因此 都会继承相应的方法,必须谨慎,不推荐使用,因为这个会改变class的原始定义
影响到所有实例
*/
// 不存在变量提升,class 不存在变量提升,hoist,这一点与ES5 完全不同。
// new Foo(); 报错
// class Foo{};
/*
上面代码中,Foo 类使用在钱,定义在后面,这样就会报错
*/
// class 表达式
const MyClass = class me{
getClassName(){
// return Me.name;
}
};
//上面代码使用表达式定义了一个类,需要注意的是,这个类的名字是myClass而不是 Me
// Me 只在Class内部代码可用,指定当前类
let inst = new MyClass();
inst.getClassName();
// Me.name ; // 报错
// 上面代码表示,ME只在Class 内部有定义,如果类的内部没有用到的话,可以省略,也就是协成
// 下面的行形式
// const MyClass = class{ /* ssss*/};
// 采用class表达式 可以写出 立即执行的class。
let person = new class {
constructor(name){
this.name = name;
}
sayName() {
console.log(this.name);
}
}('宋仁泽');
person.sayName(); //宋仁泽
// 私有方法-- 私有方法是常见的需求,但是 ES6 不提供 ,只能通过变通的方法模拟实现
// ----第一种方法 是在命名上加以区别。
class Widget{
//共有方法
foo(baz){
this._bar(baz);
}
//私有方法
_(bar){
return this.snaf = baz;
}
//..
}
/*
上面代码中,bar方法前面的下划线,表示这是一个只限于内部使用的私有方法,
凡是,这种命名是不保险的,在类的外部还是可以调用到这个方法
另一种方法:就是索性将私有方法移出模块,因为内部的所有方法 都是对外可见的
*/
class Widget2{
foo(baz){
bar.call(this,baz);
}
}
function bar(baz){
return this.snaf = baz;
}
// 还有一种方法 就是利用symbol 值的唯一性,将私有方法名字命名为一个symbol值
/*
const bar = Symbol('bar');
const sanf = Symbol('sanf');
export default class myClass{
//共有的方法
foo(baz){
this[bar](baz);
}
//私有的方法
[bar](baz){
return this[snaf] = baz;
}
}
*/
// 上面代码中 ,bar 和 snaf 都是Symbol 值,导致第三方无法获取到他们,因此
// 达到了私有的方法和属性的效果
// 类方法内部如果含有this,那它默认指向类的实例,但是,必须非常小心一旦单独使用
// 改方法很有可能报错。
class Logger{
printName( name = 'there'){
this.print(`hello ${name}`);
}
print(text){
console.log(text);
}
}
const logger = new Logger();
// const { printName } = logger;
// printName();// 报错
/*
上面代码中,printName 方法中的 this ,默认指向logger类的实例,但是,如果
将这个方法提出来单独使用,this会指向该方法运行时所在的环境,因为找不到print方法
而导致的错误
一个比较简单的解决方法是,在构造函数中绑定 this,这样,就不会找不到print方法了
*/
class Logger2 {
constructor(){
this.printName = this.printName.bind(this);
}
//...
}
// ---另一种解决方法是,使用箭头函数
class Logger3 {
constructor(){
this.printName = (name = 'there') =>{
this.print(`hello ${name}`);
};
}
}
//还有一种方法 是 使用 proxy ,获取方法的时候自动绑定this
// 严格模式
/*
类和模块内部,默认就是严格模式,所以 不需要使用 use strict 指定运行模式
只要你的代码写在类 或者模块之中,就只有严格模式可以用
考虑到未来所有的代码,其实 都是运行在模块之中的,所以 ES6 实际上
把整个语言升级到了严格模式。
由于本质上,ES6 的类只是 ES5 构造函数的一层包装, 所以函数的许多特性 都被Class
继承,包括 name 属性
*/
class Point5{};
console.log(Point5.name); //'point5';
// class 的继承
/*
基本用法 class 之间可以通过 dxtends 关键字继承,这比ES5 的通过修改原型实现的继承
要清晰和方便很多
class colorPoint extends Point{}
通过extends 关键字,继承了point 类的所有属性和方法,但是由于没有部署任何代码
所以这两个类完全一样,等于赋值了一个point类,下面,我们在color
point 内部加上代码
*/
// class ColorPoint extends Point{
// construct(x,y,color){
// super(x,y); //调用父类的constructor(x,y)
// this.color = color;
// }
// toString(){
// return this.color + '' + super.toString(); //调用父类的 toString()
// }
// }
/*
子类必须在constructor 方法中 调用 super 方法,否则 新建实例就会从报错,这是因为
子类没有自己的this对象,继而继承父类的this对象,然后对其加工,如果不调用Super方法
,子类就得不到this对象
*/
// class Point{}
// class ColorPoint extends Point{
// constructor(){
// }
// }
// let cp = new ColorPoint(); 会报错, 因为没有this
/*
以上代码中 colorPoint 继承了Point 但是他的构造函数没有调用super 方法,导致
新建的实例报错
ES5 ,实质上是先创造实例对象的this ,然后再将父类的方法 添加到this上面 Parent.apply(this)
ES6 的继承机制完全不同,市值是先创造符类的实例对象 this 然后 任何一个子类
都有constructor 方法
constructor(...args){
super(...args);
}
另一个需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this
关键字,否则会报错,这是因为子类实例的构建 是基于对父类实例的加工,只有super
方法 才能返回父类的实例。
*/
class Point6{
constructor(x,y){
this.x = x;
this.y = y;
}
}
class ColorPoint2 extends Point6{
constructor(x,y,color){
// this.color = color ; //报错
super(x,y);
this.color = color; //正确
}
}
/*
上面代码中,子类的constructor 方法 没有调用super之前,使用this关键字,结果
报错,在房子super方法之后的就是正确的。
*/
let cp = new colorPoint(25,5,'green');
cp instanceof ColorPoint //true;
cp instanceof Point6 //true;
// 实例对象cp 同时是 colorPoint 和 point 两个类的实例,这与ES5的行为完全一致
// ----类的prototype 属性和__proto__属性
/*
大多数浏览器的ES5 实现中,每一个对象都有__proto__属性,指向对应的构造函数的
prototype属性。 Class作为构造函数的语法糖,同时有prototype 属性 和__proto__
属性
1.子类的__proto__属性 ,表示构造函数的继承,总是指向父类
2.子类的prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype
属性
*/
// class A{
// }
// class B extends A {}
// B.__proto__ === A //true
// B.prototype.__proto__ = A.prototype; //true
// 以上代码中 ,子类B 的 __proto__属性指向 父类 A
// 子类B 的 prototype 属性的 __proto__属性指向父类A 的 prototype属性
// extends 关键字 后面可以跟多种类型的值
// class B1 extends A1{
// }
/*
上面的代码A 只要是一个有prototype 属性的函数,就能被B 继承,由于函数都有
prototype属性 除了function.prototype 函数,因此 A可以是任意函数
下面我们讨论三种特殊的情况
第一种情况 子类继承Object类
*/
// class A extends Object{
// }
// A.__proto__ === Object{
// }
// A.__proto__ === Object //true
// A.prototype.__proto__ === Object.prototype //true
// 这种情况下 A 其实就是 构造函数Object的复制,A的实例就是 Object的实例
// 第二种情况,不存在任何继承
// class A2{};
// A2.__proto__ === Function.prototype;
// A2.prototype.__proto__ === undefined; //true;
// 这种情况 与第二种情况 非常像,A也就是一个普通的函数,所以直接
// 继承Function.prototype,但是 A调用后返回的对象不继承任何方法,所以
// 他的__proto__指向Function.prototype.即实质上执行了下面的 代码
// class C ectends null{
// constructor(){
// return object.create(null);
// }
// }
// Object.getPrototypeOf()
// Object.getPrototypeOf(A) === Point ;
// Object.getPrototypeOf方法从子类上获取父类
// 因此 可以判断这 一个类是否继承了另一个类
// -------super-------
// super关键字
// super这个关键字既可以当做函数使用 也可以当做对象使用,这两种情况下
// 他的用法 完全不同,
// 第一种情况
// class A3(){};
// class B3 extends A3{
// construct(){
// super();
// }
// }
// 上面代码中。子类B 的构造函数之中的Super 代理表调用父类构造函数,这是必须的
// 否则JavaScript 引擎会报错。
/*
注意,super 虽然 代表了父类A的构造函数,但是返回的是子类 B 的实例,即super
内部的this指向B 因此super() 在这里相当于 A.peototype.constructor.call(this)
*/
class A3{
constructor(){
console.log(new.target.name);
}
}
class B3 extends A3{
constructor(){
super();
}
}
new A();
new B();
/*
上面代码中,new.target 指向当前正在执行的函数
可以看到 在super()执行时,他指向的是子类B的构造函数 而不是a的构造函数
也就是说,super() 内部的this指向的B
作为函数 是 ,super()只能用在子类的构造函数之中,用在其他地方就会报错
如果 super 用在子类的方法之中 就会报错
第二种情况 ,super 作为 对象时,指向父类的原型对象
*/
class A {
P(){
return 2
}
}
class B extends A {
constructor(){
super();
console.log(super.p());
}
}
let b = new B();
/*
在上面代码中 子类 B 中的super()就是将super()当成一个对象使用,这时
super指向A.prototype,所以Super.P()相当于 A.prototype.p()
仅在这里需要注意的是,由于super指向父类的原型对象,所以定义在(实例方法)方法上的方法
或者属性是无法通过super调用的
*/
class A {
construct(){
this.p = 2;
}
}
class B extends A{
get m(){
return super.p;
}
}
let b = new B()
b.m // undefined ,
// 上面代码中 P是父类A的实例属性 ,子类的实例对象就引用不到它。
// 如果属性定义在父类的原型对象上,super就可以访问取到
class A {}
A.prototype.x = 2;
class B extends A{
constructor(){
super();
console.log(super.x) //2
}
}
let b = new B();
修饰器
1.类的修饰
/*
修饰器(Decorator)是一个函数,用来系应该类的行为,这是ES7的一个提案
目前Babel 转码器已经支持。
修饰器对类的行为的改变,是代码编译时候发生的,而不是在运行时,这意味着,
修饰器能在编译阶段运行代码。
*/
function testable(target){
target.isTestable = true;
}
@testable
class MyTestableClass{}
console.log(MyTestableClass.isTestable);true
基本上,修饰器的行为就是这样
@decorator
class A{}
等同于
class A{}
A = decorator(A) || A;
也就是说,修饰器 本质就是 编译时执行的函数。修饰器函数的第一个参数,就是索要修饰的
目标类
function testable(target){
....
}
上面代码中 teatable函数的参数 target ,就是会被修饰的类。
如果你觉得一个参数不够用,可以在修饰器外面再封装一层函数。
function testable(isTestable){
return function(target){
target.isTestable = isTestable;
}
}
@testable(true)
class MyClass {}
MyClass.isTestable ;true;
@testable(false)
class MyClass_a{}
MyClass_a.isTestable ;false
上面代码中,修饰器的teatable 可以接受参数,这就等于可以修改修饰器的行为。
前面的例子 是为类添加一个静态的属性,如果想添加实例属性,可以通过类的prototype
对象操作。
function testable(target){
target.prototype.isTestable = true;
}
@testable
class myTestableClass{}
let obj = new MyTestableClass();
obj.isTestable; true
上面代码中,修饰器函数 testable是在目标类的prototype对象上添加属性,因此,就可以
在实例上面调用
下面是另一个例子
mixins.js
export function mixins(...list) {
return function(target){
Object.assign(target.prototype,...list)
}
}
mian.js
import {mixins} from './mixins'
const Foo = {
foo(){console.log('foo')}
};
@mixins(Foo)
class MyClass {}
let obj = new MyClass();
obj.foo() foo
上面代码通过修饰器mixins ,把 Foo 类的方法添加到了MyClass的实例上面,可以用
object.assign(),模拟这个功能
const Foo = {
foo(){console.log('foo')}
}
class MyClass{}
Object.assign(MyClass.prototype,Foo);
let obj = new MyClass();
obj.foo(); 'foo';
-----修饰器不仅可以修饰类,还可以修饰类的属性。
class Person{
@readonly
name(){return `${this.first} ${this.last}`}
}
上面代码中,修饰器,readonly 用来修饰类的 name 方法。
/*
此时,修饰器函数一共可以接受三个参数,
第一个参数,就是 所要要修饰的目标对象,
第二个参数是索要修饰的属性名,
第三个参数 是该属性的描述对象。
*/
function readonly(target,name,descriptor){
decorator.writable = false;
return decorator;
}
readonly(Person.prototype,'name',descriptor);
类似于
Object.definePeoperty(Person.prototype,'name',decorator);
/*
上面代码说明,修饰器,会修改属性的描述符对象,然后被修改的描述符对象
再用来定义属性。
下面是另一个例子,修改描述符对象的enumerable属性,使得该属性不可遍历
*/
class Peroson{
@noenumerable
get kidCount(){return this.children.length;}
}
function noenumerable(target,name,decorator){
decorator.enumerable = false;
return decorator;
}
下面是@log 修饰器,可以起到输出日志的作用
class Math{
@log
add(a,b){
return a+b;
}
}
function log(target,name,descriptor){
var oldValue = descriptor.value;
descriptor.value = function(){
console.log(`Calling "${name}" with`,arguments);
return oldValue.apply(null,arguments);
}
return descriptor;
}
const math = new Math();
math.add(2,3);
以上代码中,log修饰器的作用就是在 在执行原始操作之前,执行一次console.log.
从而达到输出日志的目的。
修饰器 有注释的作用
@testable
class Person{
@readonly
@noenumerable
name(){ return `${this.first} ${this.last}`}
}
从上面代码中,我们一眼就能看出,person类是可以测试的,而name方法是只读和
不可枚举的,如果同一个方法有多个修饰器,就会像剥洋葱一样,先从外到内进入,然后
由内向外执行。
function dec(id){
console.log('evaluated',id);
return (target,property,descriptor) => console.log('executed',id);
}
class Example{
@dec(1)
@dec(2)
method(){}
}
evaluated 1
evaluated 2
executed 2
executed 1
/*
上面代码中,外层修饰器@dec(1)先进入,但是内层修饰器@(2)先执行
除了注释,修饰器还能用来类型检查,所以,对于类来说,这项工能相当有用,
长期来看,他是将JavaScript代码静态分析的重要工具。
*/
为什么修饰器不能用于函数?
修饰器智能用于类和方法,不能用于函数,因为存在函数提升。
var counter = 0 ;
var add = function(){
counter ++;
}
@add
function foo(){
}
上面代码意图是执行后 counter 等于1 ,但是实际的结果是counter等于0,因为
存在函数提升,是的实际执行的代码是下面这样。
@dd
function foo(){
}
var counter;
var add;
counter = 0;
add = function(){
counter ++;
};
----重要
总之 由于函数存在函数提升,是的修饰器不能用于函数,类是不会提升的,所以
就没有这方面的问题
/*
4, core-decorators.js
core-decorator.js,是一个 第三方模块,提供了常见的修饰器,通过他,可以更好的
理解修饰器。
*/
1.@autobind
autobind 修饰器使得方法中的this对象,绑定原始对象
import {autobind} from 'core-decorators.js';
class Person{
@autobind
getPerson(){
return this;
}
}
let person = new Person();
let getPerson = person.getPerson;
getPerson() === person; true
autobind 修饰器使得方法中的this 对象,绑定原始对象。
@readonly : 使得 属性或方法不可写
import {readonly} form 'core-decorators.js';
class Meal{
@readonly
entree = 'steak';
}
var dinner = new Meal();
dinner.entree = 'salom';
Connot assign to read only property 'entree' of [object object]
@override :此修饰器 是检查 子类的方法,是否正确覆盖了父类的同名方法,如果
不正确就会报错。
import {override} from 'core-decorators';
class Parent{
speak(first,second){}
}
class Child extends Parent{
@override
speak(){}
SyntaxError,
}
class Child extends Parent{
@override
speaks(){}
SyntaxError,no matching speaks()
}
4, deprecate 或者是 deprecated 修饰器在控制台显示一条警告,表示该方法即将废除
import {deprecate} from 'core-decorators';
class Person{
@deprecate
facepalm(){}
@deprecate(' we stoped facepalming')
facepalmHard(){}
@deprecate('we stoped facepalming',{url:'http:baidu.com'})
facepalHarder(){}
}
let person = new Person();
person.facepalm();
DEPRECATION Person#facepalm: This function will be removed in future versions.
person.facepalmHard();
we stoped facepalming'
person.facepalHarder();
we stoped facepalming:http:baidu.com
5 @supperessWarnings
superessWarnings 修饰器抑制 decorated 修饰器导致的 console.warn()调用
但是异步代码发出的调用除外
import{superessWarnings} from 'core-decorators';
class Person{
@deprecated
facepalm(){}
@superessWarnings
faceWaring(){
this.facepalm();
}
}
let person = new Person();
person.facepalmWithoutWarning();
使用修饰器自动发布事件
我们可以使用修饰器,使得对象的方法被调用时,自动发出一个事件。
import postal from'postal/lib/postal.loadsh';
export default function public(toppic,channel){
return function (target,name,descriptor){
const fn = decorator.value;
decorator.value = function(){
let value = fn.apply(this,arguments);
postal.channel(channel || target.channel || '/').publish(toppic,value);
}
}
}
/*
Mixin
在修饰器的基础上,可以实现Mixin模式,所谓 mixin模式, 就是对象继承的一种替代
方案, 意为 在一个对象之中汇入另一个对象的方法。
请看下面的例子
*/
const Foo = {
foo(){console.log('foo')}
};
class MyClass {}
Object.assign(MyClass.prototype,Foo);
let obj = new MyClass();
obj.foo() foo
/*
上面代码之中,对象Foo有一个foo方法,通过Object.assign 方法,可以将foo
混入MyClass类,导致MyClass的实例obj对象都具有foo方法,这就是 混入模式的
一个简单的实现。
下面,我们部署一个简单的脚本mixins。js j将mixin 写成一个修饰器
*/
export {mixins} form './mixins';
const Foo = {
foo(){console.log('foo')}
};
@mixins(Foo)
class MyClass{}
let obj = new MyClass();
obj.foo() 'foo'
不过 上面的代码 MyClass类的prototype对象,如果你不喜欢这一点,也可以通过国
类的继承来实现mixin。
class MyClass extends MyBaseClass{
}
/*
上面代码中,MyClass继承了baseClass。如果我们想在Myclass里面混入一个 foo
方法,一个办法是在 myClass和MyBaseClass之间插入一个混入类,这个类具有foo
方法,并且继承了 MyBaseClass的所有方法,然后 MyClass再继承这个类。
*/
let MyMixin = (superClass)=>class extends superClass{
foo(){
console.log("foo from MyMixin")
}
}
接着 ,目标类再去继承这个混入类,就达到了 混入 “foo”方法的目的
class MyClass extends MyMixin(MyBaseClass){
/*...*/
}
let c = new MyClass();
c.foo(); foo from myMixin
如果需要混入多个方法,就生成多个混入类。
class MyClass extends Mixin(MyMixin2(myBaseClass)){
....
}
这种写法有一个好处,就是可以调用super,因此,可以避免 在 “混入”过程
中覆盖父类的同名方法。
let Mixin1 = (superClass) => class extends superClass{
foo(){
console.log('foo form Mixin1');
if(super.foo) super.foo();
}
};
let Mixin2 = (superClass) => class extends superclass{
foo(){
console.log('foo from Mixin2');
if(super.foo) super.foo();
}
} ;
class s {
foo(){
console.log('foo from s');
}
}
class c extends Mixin1(Mixin2(s)){
foo(){
console.log('foo from c');
super.foo();
}
}
new c().foo();
foo from c
foo from Mixin1
foo from Mixin2
foo from Mixin s
Trait --------trait 也是一种修饰器,效果与Mixin类似,但是提供更多工功能
比如防止同名方法冲突,排除混入某些方法,为混入的方法起别名等等。
import {traits} from 'traits-decorator';
class TFoo {
foo(){console.log('foo')}
}
const TBar = {
bar(){console.log('bar')}
foo(){console.log('foo')} 有了同名方法 就会报错
}
@traits(TFoo,TBar::excludes('foo'))使用绑定运算符 在Tbar上排除 foo
class MyClass{}
let obj = new Myclass();
obj.foo(); foo
obj.bar(); bar
上面代码中,通过trait是修饰器,在 MyClass类上面 混入了TFoo 类的 foo
和Tbar对象的bar 方法
Trait不允许 混入同名的方法。
有一种解决方法就是排除TBbar的 foo方法。
@traits(TExample::excludes('foo','bar')::alias({baz:'exampleBaz'}))
class MyClass {}
上面代码排除了TExample的foo方法和bar方法,为baz方法起了别名exampleBaz。
as方法则为上面的代码提供了另一种写法。
@traits(TExample::as({excludes:['foo', 'bar'], alias: {baz: 'exampleBaz'}}))
class MyClass {}