重学JavaScript的第3天

364 阅读4分钟

Ch7 迭代器与生成器

7.2 迭代器

将实现了Iterable接口并且可以通过Iterator迭代器消费的结构成为可迭代对象

迭代器(iterator) 是按需创建的一次性对象。每个迭代器都会关联一个可迭代对象。

迭代器无须了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值。

7.2.1 可迭代协议

实现可迭代协议,必须要具备两种能力:

  • 支持迭代的自我识别
  • 创建实现Iterator接口的独享

一些具有迭代协议的内置类型:

  • 字符串
  • 数组
  • Map
  • Set
  • arguments对象

7.2.2 迭代器协议

迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器 API 使用 next() 方法 在可迭代对象中遍历数据。每次成功调用 next(),都会返回一个 IteratorResult 对象,其中包含迭代器返回的下一个值。

每个迭代器都表示对可迭代对象的一次性有序遍历。不同迭代器的实例相互之间没有联系,只会独立地遍历可迭代对象。

const arr = ['foo','bar'];
const it = arr[Symbol.iterator]();
const it2 = arr[Symbol.iterator]();
console.log(it === it2);        // false
console.log(it.next());         // {value: 'foo', done: false}
console.log(it2.next())         // {value: 'foo', done: false}
console.log(it.next());         // {value: 'bar', done: false}
console.log(it.next());         // {value: undefined, done: true}
console.log(it.next());         // {value: undefined, done: true}

如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化:

const arr = ['foo','bar'];
const it = arr[Symbol.iterator]();
console.log(it.next());         // {value: 'foo', done: false}
​
arr.splice(1,0,'baz');
console.log(arr);               //  ['foo', 'baz', 'bar']
console.log(it.next());         // {value: 'baz', done: false}
console.log(it.next());         // {value: 'bar', done: false}

7.2.3 自定义迭代器

与 Iterable 接口类似,任何实现 Iterator 接口的对象都可以作为迭代器使用。看下例:

class CountIter{
    constructor(limit){
        this.limit = limit;
    }
    
    [Symbol.iterator](){
        let count = 1;
        let limit = this.limit;
        return {
            next(){
                if(count <= limit) {
                    return {done:false, value:count++}
                } else {
                    return {done:true, value:undefined}
                }
            }
        }
    }
}
​
let count = new CountIter(5);
for(let item of count) {
    console.log(item);
}
// 1
// 2
// 3
// 4
// 5

7.3 生成器

生成器的形式是一个函数,函数名称前面加一个星号(*) 表示它是一个生成器。只要是可以定义函数的地方,就可以定义生成器。

// 生成器函数声明
function* generatorFn() {} 
// 生成器函数表达式
let generatorFn = function* () {} 

注意:箭头函数不能定义生成器函数

7.3.2 yield

yield 关键字可以让生成器停止和开始执行,也是生成器最有用的地方。生成器函数在遇到 yield 关键字之前会正常执行。遇到这个关键字后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用 next()方法来恢复执行

生成器实例:

function* genFn() {
    for (let index = 0; index < 5; index++) {
        yield index;
    }
    return "foo";
}
​
const gen = genFn();
let i = gen.next();
console.log(i);           // { value: 0, done: false }
i = gen.next();           // 
console.log(i);           // { value: 1, done: false }
i = gen.next();           // 
console.log(i);           // { value: 2, done: false }
i = gen.next();           // 
console.log(i);           // { value: 3, done: false }
i = gen.next();           // 
console.log(i);           // { value: 4, done: false }
i = gen.next();           // 
console.log(i);           // { value: 'foo', done: true }
i = gen.next();           // 
console.log(i);           // { value: undefined, done: true }

Ch8 对象、类与面向对象编程

8.1 理解对象

简单来说,对象由方法和属性构成。

8.1.1 属性类型

一些内部特征来描述属性的类型:主要分为数据属性和访问器属性

数据属性

  • [[Configurable]]:表示属性是否可以删除并重新定义,是否可以修改等,默认情况下为true
  • [[Enumerable]]:表示属性是否可以通过for-in循环返回。默认是true
  • [[Writable]]:表示属性的值是否可以被修改。默认是true
  • [[Value]]:包含属性实际的值。默认是undefined。

通过defineProperty来修改属性的默认特性:

let person = {};
Object.defineProperty(person, "name", {
  writable: false,
  value: "Garfield"
})
console.log(person["name"]);            // Garfield
person["name"] = "Abc";         
console.log(person["name"]);            // Garfield

访问器属性

  • [[Get]]:获取函数,在读取属性时调用

  • [[Set]]:设置函数,在写入属性时调用

    以上这两个属性其实不是必须的,是开发者在需要的时候另外配置的(例如vue2.x)

阅读以下代码,我们并没有修改显示地修改edition属性,而是通过year属性的访问器属性中的set来间接地改变edition。

let book = {
  year_: 2017,
  edition:1
}
​
Object.defineProperty(book, "year", {
  get(){
    return this.year_;
  },
  set(newValue) {
    if (newValue > 2017) {
      this.year_ = newValue;
      this.edition += newValue - 2017;
    }
  }
})
​
book.year = 2018;
console.log(book.edition);    // 2

8.1.4 对象合并

通过Object.assign方法来实现对象合并。

需要注意的是:

  • Object.assign()实际上对每个源对象执行的是浅复制
  • 如果多个源对象都有相同的属性,则使用最后一个值
  • 不能在两个对象间转移获取函数(get)和设置函数(set)。
let src = { a: {}, id: 1 };
let dest = { id: 2 };
​
let result = Object.assign(dest, src);
​
console.log(result === dest);     // true
console.log(result.a === src.a);  // true,这里就是浅复制
console.log(result.id);           // 1

8.2 创建对象

熟知的创建对象:

function Person(name, age, job){ 
 this.name = name; 
 this.age = age; 
 this.job = job; 
} 
let person1 = new Person("Nicholas", 29, "Software Engineer");

new操作符发生的事情:

  • 内存中创建一个对象
  • 修改这个对象的[[prototype]],使其指向Person的prototype
  • 修改内部的this指针,指向刚创建的对象
  • 执行构造函数中的代码
  • 返回刚创建的新对象

8.3 继承

这里主要就记录一下原型链继承,其实原理是将子类的prototype上的[[prototype]]绑定到父类上去。

function SuperType() { 
 this.property = true; 
} 
SuperType.prototype.getSuperValue = function() { 
 return this.property; 
}; 
function SubType() { 
 this.subproperty = false; 
} 
// 继承 SuperType 
SubType.prototype = new SuperType(); 
SubType.prototype.getSubValue = function () { 
 return this.subproperty; 
}; 
let instance = new SubType(); 
console.log(instance.getSuperValue()); // true 

原型链如下图:

1.png

8.4 类

ES6引入了class关键字,让JavaScript看起来是一门面向对象的语言。而实际上class的本质还是原型链。

用法与面向对象的语言基本一致

  • 继承:extends关键字,super关键字
  • 静态方法:static
  • 抽象类:new.target
class Vehicle { 
    constructor() { 
        console.log(new.target); 
        if (new.target === Vehicle) { 
            throw new Error('Vehicle cannot be directly instantiated'); 
        } 
    } 
} 
// 派生类
class Bus extends Vehicle {} 
new Bus(); // class Bus {} 
new Vehicle(); // class Vehicle {} 
// Error: Vehicle cannot be directly instantiated 

Ch9 代理与反射

ECMAScript 6 新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力。

代理与反射不支持转义成低版本的ES代码,因此只能在完美支持该机制的浏览器或者其他环境下运行代码。

9.1 代理基础

代理实际上是目标对象的一个抽象。

9.1.2 捕获器 trap

使用代理的主要目的就是在访问目标对象的时候进行额外操作,而这些额外操作的实现就是通过定义捕获器来实现,每个捕获器代表一种基本操作。例如下面代码中的 get 捕获器,表示在通过proxy访问对象属性的时候,会执行捕获器中的代码。

const target = { id: "123" };
​
const handler = {
    get() {
        return '哈哈哈,被捕获器拦截啦!';
    },
};
​
const proxy = new Proxy(target, handler);
console.log(proxy.id); // 哈哈哈,被捕获器拦截啦!

9.1.3 捕获器参数与反射

所有的捕获器可以访问相应参数,例如 get 捕获器,可以接收 3 个参数,分别是:目标对象、访问属性、代理对象

const target = { 
 foo: 'bar' 
}; 
const handler = { 
 get(trapTarget, property, receiver) { 
 console.log(trapTarget === target);            // true
 console.log(property);                         // foo
 console.log(receiver === proxy);               // true
 } 
}; 
const proxy = new Proxy(target, handler); 
proxy.foo; 

什么是反射?反射其实是相对于捕获器而言的。一个捕获器对应一个反射,例如 get 捕获器的反射是 Reflect.get 这个方法。反射 API 为开发者准备好了样板代码,在此基础上开发者可以用最少的代码修改捕获的方法。

const target = { 
 foo: 'bar', 
 baz: 'qux' 
}; 
const handler = { 
 get(trapTarget, property, receiver) { 
 let decoration = ''; 
 if (property === 'foo') { 
 decoration = '!!!'; 
 } 
 return Reflect.get(...arguments) + decoration; 
 } 
}; 
const proxy = new Proxy(target, handler); 
console.log(proxy.foo);  // bar!!! 
console.log(target.foo); // bar 
console.log(proxy.baz);  // qux 
console.log(target.baz); // qux

9.1.5 可撤销代理

const target = { 
 foo: 'bar' 
}; 
const handler = { 
 get() { 
    return 'intercepted'; 
 } 
}; 
const { proxy, revoke } = Proxy.revocable(target, handler); 
console.log(proxy.foo); // intercepted 
console.log(target.foo); // bar 
revoke(); 
console.log(proxy.foo); // TypeError