SKT-otto

导航

ES6(3)


1 、ECMAScript6新增数据类型symbol数据类型
1.1 Symbol介绍
在介绍Symbol之前,我们简单介绍一下JavaScript的数据类型:
JavaScript有6中数据类型,分别是:String字符串类型;Number数字类型;Object对象类型;Boolean布尔值类型;Null空值;Undefined 未定义;
这6种类型写过代码的同学都不会陌生,它们都有各自的用途。而ES6给我们带来一种全新的数据类型:Symbol。每一种全新的事物的诞生都是为了解决某种问题。Symbol的初衷:解决对象的属性名冲突。
 
从上面代码案例看到,我们用一个Symbol( )函数来创建一个symbol类型的变量,我们打印了一下变量sm,得到的结果是控制台输出:Symbol( ),它代表着一个独一无二的值,虽然我们看不到它长什么样子,但基本上,它有点类似字符串。
接着,我们用typeof来检测一下变量sm的类型,得到的结果是:symbol。
 
我们定义两个symbol类型的变量sm1,sm2,然后用全等符号===(也称严格相等,第九节讲解过)进行比较,得到的是false。也就是他们都是独一无二的值,并不相等。
 
        接着,我们分别打印两个变量,控制台输出的都是Symbol( ),看起来长得一模一样,实际是不相等的。
 
        两个不一样的值,控制台输出的一样,这样无疑给我们开发调试带来一定的不便,有没有办法让他们看起来不一样呢?
 
        有的,Symbo( )函数接受参数,用于对实例值的描述。我们试试看:
 
  用字符串sm1和sm2作为参数,结果打印出来的变量sm1和sm2就是Symbol(sm1)和Symbol(sm2),等于加上了描述,很容易区分出来。
 
        需要注意的是,即使参数一样,描述一样,得到的两个值也是不相等的,不信我们来看看:
 
即使两个变量的描述都是“sm”,但是终究对应的值还是不一样的,symbol永远都是独一无二的值,谨记。
 
        了解了这几个symbol类型值的特点后,前面说到,Symbol是为了解决对象属性名冲突的问题,那么我们就结合对象,来学习:
 
看代码,从上往下撸,首先,我们定义一个symbol类型的变量name,它作为一个对象person的属性,对应的值是“张三”;
        接着,我们用两种方式获取name的值,第一种用中括号的形式[ name ]能正确获取到,第二种用点运算符的形式,获取失败。原因是:当symbol值作为对象的属性名的时候,不能用点运算符获取对应的值。
此外还有一点要注意,把一个symbol类型的值作为对象的属性名的时候,一定要用中括号[ ],不能用点运算符,因为用点运算符的话,会导致javascript把后面的属性名为理解为一个字符串类型,而不是symbol类型。具体看代码:
 
其中变量name是symbol,但是给person对象设置属性的时候,用的是点运算符person.name,而不是中括号person[ name ],这会有什么后果呢?这就会导致person对象中的属性name实际上是字符串类型的,这也就解释了最后三行代码的打印结果了。
person[ name ]这句代码相当于要求javascript去person对象内找一个symbol类型的属性name,不好意思,没有,找不到。person对象只有一个字符串类型的属性name;所以,如果用person[‘name’]或者peroson.name获取的话,就能找到对应的属性name了。
 
1.2 属性名的遍历
当symbol类型的值作为属性名的时候,该属性是不会出现在for...in和for...of中的,也不会被Object.keys( )获取到。我们来看案例:
 
 person对象有两个属性,属性名有两种类型:symbol类型和string字符串类型,我们通过keys( )函数获取到的属性,只有属性age,我们通过for...in循环打印出来,也只打印出了属性age。(for...of也属于ES6的新增知识,后面会专门有一节介绍),以上几种方法都无法获取到symbol类型的属性。

1.3 Object.getOwnPropertySymbols函数
如果我们硬是想要获取symbol类型的属性怎么办?我们可以用一个方法:Object.getOwnPropertySymbols( ),它会找到symbol类型的属性并且返回一个数组,数组的成员就是symbol类型的属性值,看代码:
 
person对象的两个属性都是symbol类型的,我们也知道用Object.keys( )和for...in都无法获取到它,我们就用getOwnPropertySymbols( )方法来,结果成功了,得到一个数组,数组的内容就是两个symbol类型变量对应的值Symbol(name)和 Symbol(age)。

1.4 Reflect.ownKeys函数
由于获取字符串类型的属性和获取symbol类型的属性要分开两种不同的方式来获取,难免有有时候会很不方便,有木有什么办法让我们一次性获取所有类型的属性,不管它是字符串类型还是symbol类型呢?我们可以用Reflect.ownKeys( )方法实现:
 
 上面的代码中,我们先定义一个对象person,它含有两个属性,一个是symbol类型的,一个是字符串类型的。
接着,我们将对象person传入Reflect.ownKeys( )函数中,函数就会给我们返回一个数组,数组的内容便是对象的属性,包括symbol类型和字符串类型。

1.5 Symbol.for函数
函数作用:根据参数名,去全局环境中搜索是否有以该symbol.for()参数为名的symbol值,有就返回它,没有就以该参数名来创建一个新的symbol值。
 
  上面最后一句代码,我们用全相等来对两个变量进行对比,得到:true;说明n2就是n1,两者相等。
但是细心地同学会注意到,上面的代码中,定义两个symbol值得时候用的都是Symbol.for( ),而不是用Symbol( )。
        两者在创建symbol值的时候有什么不同吗?它们的区别是:Symbol.for( )创建的symbol值会被登记在全局环境中,供以后用Symbol.for( )来搜索,而Symbol( )创建的变量就没有这样的效果了。
        也就是说,用Symbol( )创建的symbol值,以后用Symbol.for( )去搜索,是找不到的。不信,我们来演示一下,还是用上面的代码,我们稍微改一下第一行:
 
第一行我们用Symbol( )来创建的一个symbol值,按照上述的所说的,它不会被登记在全局环境中;所以,第二行我们用Symbol.for( )去找的时候,是找不到的,找不到怎么办?此时Symbol.for( )会自动创建一个新的symbol值,也就是说n1,n2是不同的两个symbol值了,所以进行全相等比较的时候,会返回:false。

1.6 Symbol.keyFor函数
函数作用:返回一个以被登记在全局环境中的symbol值的key,没有就返回undefined。注意这句话的一个关键词:“被登记在全局环境中”,也就是说这个symbol值是被Symbol.for( )创建的,不是被Symbol( )创建的。
 
上面的变量n1是被Symbol.for( )创建,不是被Symbol( )创建的,所以用Symbol.keyFor( )去找,是能找到的,会返回这个symbol值的key,也就是它的描述:name。
 
 这段代码的变量n1是用Symbol( )创建的,最后的结果是undefined;这就证明了两个知识点:1、Symbol( )创建symbol值不会被登记在全局环境中供Symbol.for( )和Symbol.keyFor( )搜索;2、Symbol.keyFor( )函数在全局环境中找不到对应的symbol,就回返回undefined。

2 、Proxy的实现
2.1 proxy的实现
 
先定义一个对象,含有name属性,值为“张三”,创建一个代理对象pro,对象person的操作都交给代理对象pro,这不,看最后一句代码,如果你要读取person对象的name属性,就要用pro.name,而不是person的name。我们看到的结果是:“李四“而不是person对象重点张三,因为代理过程中,get方法实现了拦截的作用,不管你读取什么属性,我都返回”李四“。
        这就是代理Proxy的作用,将一个对象交给了Proxy代理,然后通过编写处理函数,来拦截目标对象的操作。上面的案例,你可以理解为,person对象的name属性值不想被别人知道是“张三“,就设置了一个代理,让别人读取的时候,只获取到”李四“。
 
2.2 set方法
而上面提到的“编写处理函数“,get方法就是其中一个,除了get方法以外,还有一个很常用的是:set方法,它用于拦截对对象的写操作。
        我们来结合银行的例子,来用get方法和set方法的实现,这段代码有点长,但是注释很详细,注意看代码注释:
 
 
几乎每一句代码都有注释,这段代码对应的故事情节是这样的:老王有的银行账户里面有一些存款,其中人民币1000元,美元0元。
 
 有一天,他来到银行柜台前,找到一个叫banker的工作人员,取款之前看看账户里面还有多少钱,然后工作人员banker开始帮他操作(也就是代理)。
 
banker告诉他:“您账户里面有人民币1000元,可以取款的,但美元余额不足“。
 
接着,老王不打算取款了,打算存500美元。
在填写存款单据的时候,把500不小心写成了“五百“,banker告诫老王:”这样是写不行的,一定要写阿拉伯数字,这样写银行无法帮您存款的“。结果存款失败,账户里面的美元还是0元。
 
 没关系,马上改过来,把“五百“改成500。
 
存款成功,账户里面的美元已有500元。
        故事的整个经过就是这样,有了Proxy代理(银行工作人员bank),帮助老王完成查看银行存款和取款的操作(代理),避免了一些误操作。
        get方法拦截了读取操作,set方法拦截了改写操作。Proxy除了支持以上拦截程序,还支持一系列的拦截函数,我们选择几个常用的讲解!

2.3 ownKeys方法
ownKeys拦截操作,拦截过滤Object.ownKeys()对对象的属性遍历。
 
我们编写的ownKeys方法程序,不管你有多少属性,只返回两个属性name和age。我们看最后两行代码:Object.keys(person); 这里我们不使用代理,直接用keys( )函数遍历person对象,得到的person对象的原本属性"name"、 "age"和"height"。而Object.keys(proxy) 这句代码遍历的是被代理的proxy对象,所以,得到的只是被过滤后的结果:[“name”,”age”]。

2.4 has方法
has( )拦截操作:拦截key in object的操作,结果会返回一个布尔值。
 
  has( )方法用于是判断是否含有指定的键值对,有,就返回true。否则返回false。
        对象含有name属性,所以返回true,没有height属性,返回false。

2.5 apply方法
除了对象类型的变量可以被代理,函数也可以被代理。如果被代理的变量是一个函数,那么还会支持一个拦截程序:apply调用。
 
 最后一句代码,proxy本身是一个代理实例对象,因为它代理的是一个函数fn,所以可以直接用函数的形式调用proxy( );当它当作函数调用的时候,就会被apply拦截,执行alert('我是隔壁老王')。

2.6 proxy.revocable方法
如果创建了代理之后又想取消代理的话,我们可以用Proxy.revocable( )(可废止的,可撤回的;)函数来实现,它会返回一个对象,对象中含有一个proxy属性,它就是Proxy的代理实例对象;还有一个revoke属性,它是一个方法,用于取消代理。
 
 这个案例大家要注意的是Proxy.revocable( )方法返回的结果,它是一个对象,在控制台打印出来后的结果是:Object{ proxy:Object , revoke:function(){....} }。有一个proxy属性,它就是Proxy代理实例,还有一个属性revoke,它是一个方法,专用于取消代理。
        我们使用object.proxy.name来读取name的属性,由于被代理拦截了,只能读取到“李四”,接着我们调用revoke( )方法取消代理,然后再使用object.proxy.name的时候就会报错了,代理已经不存在了。

3 、for…of使用
3.1 for…of使用
for...of 一种用于遍历数据结构的方法。它可遍历的对象包括数组,对象,字符串,set和map结构等具有iterator 接口的数据结构。
我们先来看看几种传统的遍历数组的方式以及它们的缺陷:
方式一:
 
方式一利用for循环来遍历数组的缺点就是:代码不够简洁。
方式二:
 
利用forEach循环代码量少了很多,写法更加简洁,缺点就是:无法中断停止整个循环。
方式三:
 
 for...in循环更常用于对象的循环,如果用于数组的循环,那么就要注意了,上述代码中每次循环中得到的i是字符串类型,而不是预料中的数字类型,要想对它进行运算,那得先要进行类型转换,造成不方便。
那我们来看看for...of的是实现:
 
for...of的优势:
1. 写法比for循环简洁很多;
2. 可以用break来终止整个循环,或者continute来跳出当前循环,继续后面的循环;
结合keys( )获取到循环的索引,并且是数字类型,而不是字符串类型。

3.2 循环可以终止
 
以上案例:用break实现了终止整个循环,不会继续后面的遍历,所以打印结果为:1  2。
可以跳过当前循环:
 
用continue跳过当前循环,继续后面的循环,所以打印结果为:1  2  4  5。

3.3 得到数字类型索引
 
  使用数组的扩展keys( ),获取键名再遍历,得到的index是数字类型的。
  此外,相比于for...in循环专门为对象设计,for...of循环的适用范围更广。

3.4 遍历字符串
for...of 支持字符串的遍历。
 
 
3.5 遍历DOM list
for...of支持类数组的遍历,例如DOM List。
 
 for...of支持set和map解构的遍历,考虑到set和map我们还没学习,我们在后面章节一并讲解。

4 、Iterator遍历器
4.1 for…of为什么不遍历object对象
我们讲了简单又实用的for...of,我们可以使用它来遍历数组,字符串,Set和Map结构,但是有没有发现,我们并没有说它可以遍历Object对象,为什么不试试用它来遍历Object对象呢?
//定义一个的Object对象
let obj = {"name":"前端君"};
//咱们来for...of一下
for(let v of obj){
    console.log(v);
}
//结果:报错
//错误提示:obj[Symbol.iterator]不是一个function
  oh no,程序报错了,for...of根本不支持遍历普通的Object对象,还出现了错误提示:obj[Symbol.iterator]不是一个function,这是什么鬼?
 为什么数组,Set和Map结构又可以支持for...of的遍历呢?
原来,要想能够被for...of正常遍历的,都需要实现一个遍历器Iterator。而数组,Set和Map结构,早就内置好了遍历器Iterator(又叫迭代器),它们的原型中都有一个Symbol.iterator方法;而Object对象并没有实现这个接口,使得它无法被for...of遍历。
        那么,我们就亲自来验证一下,它们的原型中到底是不是有个叫Symbol.iterator的方法:
//数组
    Array.prototype[Symbol.iterator];
    //结果:function values(){...}
    //字符串
    String.prototype[Symbol.iterator];
    //结果:function [Symbol.iterator](){...}
    //Set结构
    Set.prototype[Symbol.iterator];
    //结果:function values(){...}
    //Map结构
    Map.prototype[Symbol.iterator];
    //结果:function entries(){...}
    //Object对象
    Object.prototype[Symbol.iterator];
    //结果:undefined
 从上往下看,确实,唯独Object对象的原型上没有Symbol.iterator,返回了:undefined。其他的数据类型的原型上都含有一个名字叫Symbol.iterator的方法Function。
 
        注意:Symbol.iterator 是Symbol 对象的 iterator 属性,是一个特殊的Symbol值,因此,当它作为prototype对象属性名的时候,获取它的时候需要使用[ ]的形式: prototype[Symbol.iterator],不能使用点形式获取:prototype.Symbol.iterator。
也就说,只要一个数据结构拥有一个叫[Symbol.iterator]()方法的数据结构,就可以被for...of遍历,我们称之为:可遍历对象。比如:数组,字符串,Set和Map结构。
        现在你该知道为什么文章第一个案例会出现错误提示:obj[Symbol.iterator]不是一个function 了吧,因为Object对象的原型上压根就没有[Symbol.iterator]() 方法啊。
 
4.2 Iterator原理
当可遍历对象被for...of遍历的时候,[Symbol.iterator]()就会被调用,返回一个iterator对象。其中还有一个很重要的方法:next( );
  //数组:一个可遍历对象
    let arr = ['a','b','c'];
    //调用数组的Symbol.iterator()方法
    let iter = arr[Symbol.iterator]();

    iter.next();
    //结果:{value: "a", done: false}
    iter.next();
    //结果:{value: "b", done: false}
    iter.next();
    //结果:{value: "c", done: false}
    iter.next();
    //结果:{value: undefined, done: true}
 第1次调用next( )方法:返回数组的第1个元素:“a”,以及done的值为fasle,表示循环没有结束,继续遍历。
 
        第2次调用next( )方法:返回数组的第2个元素:“b”,以及done的值还是为fasle,表示循环没有结束,继续遍历。
 
        第3次调用next( )方法:返回数组的第3个元素:“c”,以及done的值依然为fasle,表示循环没有结束,继续遍历。
 
        第4次调用next( )方法:返回的value值为undefined,以及done的值变成了true,表示遍历结束。
 
        原来,for...of的原理就是:先调用可遍历对象的[Symbol.iterator]( )方法,得到一个iterator遍历器对象,然后就在遍历器上不断调用next( )方法,直到done的值为true的时候,就表示遍历完成结束了。
 
4.3 自定义Iterator遍历器
既然有了[Symbol.iterator]()方法就算是可遍历对象,那么我给Object对象手动加上一个[Symbol.iterator]()方法,那么它是不是可以被for...of遍历了?
那我们就试试看,给一个Object对象加一个[Symbol.iterator]( )方法,看它是不是就能被for...of遍历了?
//定义一个的Object对象
    let obj = {
        0:"我是0",
        1:"我是1",
        2:"我是2",
        length:3,
        //添加[Symbol.iterator]方法
        [Symbol.iterator] : function() {
            let _this = this;
            let index = 0;
            return {
                next:() => {
                    let value = _this[index];
                    let done = (index >= _this.length);
                    index++;
                    return {value,done}
                }
            }
        }
    };
    //咱们来for...of一下
    for(let v of obj){
        console.log(v);
    }
    //结果:"我是0"
    //      "我是1"
    //      "我是2"
 
上面这个案例也许你看了觉得很复杂,没看懂,没关系,我一起来分析它的结构就够了!
 
        我们定义了一个Object对象,同时给它添加了[Symbol.iterator]()方法,并在[Symbol.iterator]()方法实现了next( )方法,next( )方法返回的对象包含了value属性和done属性。
 
        具体细节如果不理解没关系,我们确实看到了给Object对象加上了[Symbol.iterator]()方法后,最后确实能被for...of遍历了。
 
        这就是说,我们可以创建一个可遍历的对象,并且自定义它的遍历行为。或者说可以通过添加[Symbol.iterator]()方法,把一个不可遍历的Object对象,变成可遍历的对象。
 
4.4 Iterator遍历器的价值
新特性for...of之所以能够遍历各种不同的数据结构,正是因为这个数据结构都实现了Iterator遍历器接口,供for...of遍历。如果没有实现Iterator接口,则该数据结构无法被for...of遍历,比如:普通的Object对象。

5 、Generator函数
5.1 声明Generator函数
Generator函数,又称生成器函数,是ES6的一个重要的新特性。
//声明一个Hello的Generator函数
    function* Hello(name) {
        yield `hello ${name}`;
        yield `how are you`;
        yield `bye`;
    }
 
上面这个就是Generator函数,乍一看,是不是跟普通的函数没什么两样?确实很像,但是我们要知道它有两个重要的区别:
1. 普通函数用function来声明,Generator函数用function*声明。
Generator函数函数内部有新的关键字:yield( 产出),普通函数没有。

5.2 调用Generator函数
带着这两个疑问我们往下看,我们试着就调用一下这个名字叫Hello的Generator函数,看看会发生什么:
//声明一个Hello的Generator函数
    function* Hello(name) {
        yield `hello ${name}`;
        yield `how are you`;
        yield `bye`;
    }
    //调用Hello函数
    let ite = Hello('前端君');
    //结果:[object Generator]
    ite.next();
    //{value: "hello 前端君", done: false}
    ite.next();
    //{value: "how are you", done: false}
    ite.next();
    //{value: "bye", done: false}
    ite.next();
    //{value: undefined, done: true}
 
看到这里,估计你也看到了一个熟悉的面孔:next()方法。一开始,我们调用Hello(“前端君”),函数执行后,返回了一个:[object Genrator]生成器对象,我们把它赋值到变量ite中,仅此而已,并没有做太多的事情。
        接着,第1次调用生成器对象ite的next( )方法,返回了一个对象:
 {value: "hello 前端君", done: false}
  第2次调用生成器对象ite的next( )方法,同样得到了一个对象:
 {value: "how are you", done: false}
 
第3次调用生成器对象ite的next( )方法,又得到了一个对象:
 {value: "bye", done: false}
 
 直到,第4次调用生成器对象ite的next( )方法,返回的对象:
 {value: undefined, done: true}
看到这里有没有发现,这里生成器的next( )方法的和遍历器iterator的next( )方法的返回结果是不是一样?
 
        没错,你可以把Generator函数被调用后得到的生成器理解成一个遍历器iterator,用于遍历函数内部的状态。

5.3 Generator函数的行为
通过上面的案例,我们知道了:Generator函数被调用后并不会一直执行到最后,它是先回返回一个生成器对象,然后hold住不动,等到生成器对象的next( )方法被调用后,函数才会继续执行,直到遇到关键字yield后,又会停止执行,并返回一个Object对象,然后继续等待,直到next( )再一次被调用的时候,才会继续接着往下执行,直到done的值为true。

5.4 yield语句的使用
而yield在这里起到了十分重要的作用,就相当于暂停执行并且返回信息。有点像传统函数的return的作用,但不同的是普通函数只能return一次,但是Generator函数可以有很多个yield。而return代表的是终止执行,yield代表的是暂停执行,后续通过调用生成器的next( )方法,可以恢复执行。

5.5 next方法接收参数
此外,next( )方法还可以接受一个参数,它的参数会作为上一个yield的返回值,我们来看一下:
    //声明一个Hello的Generator函数
    function* Hello() {
        let res = yield `hello`;
        yield res;
    }
   
    let iterator = Hello();
    //结果:一个生成器对象
    iterator.next();
    //结果:{value: "hello", done: false}
    iterator.next("前端君");
    //结果:{value: "前端君", done: false}
        注意函数体内的第一个yield关键字,我们把它的返回值赋值给了一个变量res。
        再看2次next方法的调用:
        第1次调用next( )方法,返回的对象属性value值为“hello”,属性done值为:fasle,并暂停执行。
        第2次next( )方法,传入参数:字符串“前端君”。此时,第二个yield关键字紧跟着的是变量res,而变量res的值正是上一个关键字yield的返回值。也就是说这个值正是我们传入的参数:“前端君”。因为:next( )的参数会作为上一个yield的返回值。

5.6 关键字‘yield*’
在一个Generator函数里面,如果我们想调用另一个Generator函数,就需要用到的关键字是:yield*。
        我们来看看怎么玩,代码有点长,但是很好理解:
    //声明Generator函数:gen1  
    function* gen1() {
        yield "gen1 start";
        yield "gen1 end";
    }
    //声明Generator函数:gen2
    function* gen2() {
        yield "gen2 start";
        yield "gen2 end";
    }
    //声明Generator函数:start
    function* start() {
        yield "start";
        yield* gen1();
        yield* gen2();
        yield "end";
    }
    //调用start函数
    var ite = start();
    //创建一个生成器
   
    ite.next();
    //{value: "start", done: false}
    ite.next();
    //{value: "gen1 start", done: false}
    ite.next();
    //{value: "gen1 end", done: false}
    ite.next();
    //{value: "gen2 start", done: false}
    ite.next();
    //{value: "gen2 end", done: false}
    ite.next();
    //{value: "end", done: false}
   
        我们主要看start( )这个Generator函数,其中有两句代码:
    yield* gen1();
    yield* gen2();
        这里使用了关键字yield*来实现调用另外两个Generator函数。从后面的多个next( )方法得到的结果看,我们可以知道:
        如果一个Generator函数A执行过程中,进入(调用)了另一个Generator函数B,那么会一直等到Generator函数B全部执行完毕后,才会返回Generator函数A继续执行。

5.7 Generator函数的用途
以上就是对Generator函数的讲解介绍,它是ES6的一个很重要的新特性。它可以控制函数的内部状态,依次遍历每个状态;可以根据需要,轻松地让函数暂停执行或者继续执行。
        根据这个特点,我们可以利用Generator函数来实现异步操作的效果。
        原理是:利用Generator函数暂停执行的作用,可以将异步操作的语句写到yield后面,通过执行next方法进行回调。

posted on 2020-08-06 20:11  SKT-otto  阅读(79)  评论(0编辑  收藏  举报