7 迭代器和生成器
## 迭代器
迭代器和生成器是一种抽象概念 目的是动态产生数据 而不是一次性全部计算完成
js 的迭代器把可迭代对象和迭代器做分离,迭代器只关心如何取得下一个值,不关系迭代对象的具体数据结构
每次成功调用 next(),都会返回一个 IteratorResult 对象,其中包含迭 代器返回的下一个值。若不调用 next(),则无法知道迭代器的当前位置。
next()方法返回的迭代器对象 IteratorResult 包含两个属性:done 和 value。
return 和 throw 直接结束迭代
迭代器不是快照,所以原来的对象如果数据变化,迭代器下一步之类的会跟着变化
在闭包中实现自定义迭代器
class Counter {
constructor(limit) {
this.limit = limit;
}
[Symbol.iterator]() {let count = 1,
limit = this.limit;
return {
next() {
if (count <= limit) {
return { done: false, value: count++ };
} else {
return { done: true, value: undefined };
}
} };
} }
let counter = new Counter(3);
for (let i of counter) { console.log(i); } // 1
// 2
// 3
for (let i of counter) { console.log(i); }
// 1
// 2
// 3
这是因为 `count++` 是**后置递增操作符**,它会先返回当前的 `count` 值,然后再将 `count` 自增 1。这是后置操作符的特性,和前置递增操作符 `++count` 的行为不同。
生成器
// 生成器函数声明
function* generatorFn() {}
// 生成器函数表达式
let generatorFn = function* () {}
// 作为对象字面量方法的生成器函数
let foo = {
* generatorFn() {}
}
// 作为类实例方法的生成器函数
class Foo {
* generatorFn() {}
}
// 作为类静态方法的生成器函数
class Bar {
static * generatorFn() {}
}
yield 关键字
生成器函数在遇到 yield 关键字之前会正常执行。遇到这个关键字后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用 next()方法来恢复执行:
function* generatorFn() {
yield 'foo'; yield 'bar'; return 'baz';
}
let generatorObject = generatorFn();
console.log(generatorObject.next()); console.log(generatorObject.next()); console.log(generatorObject.next());
// { done: false, value: 'foo' } // { done: false, value: 'bar' } // { done: true, value: 'baz' }
把生成器对象当成可迭代对象, 那么使用起来会更方便
yield 关键字还可以作为函数的中间参数使用
生成器的意义总结
- 延迟执行(惰性计算):按需生成数据,尤其适合处理大数据或无限数据流。
- 简化逻辑:用于实现复杂流程(如状态机)或自定义迭代器,代码更简洁直观。
- 异步编程的雏形:在
async/await
出现之前,生成器是异步编程的核心工具。 - 节省资源:避免一次性计算或加载所有数据,提高性能。
傻B才一页一页看书!
8 面向对象编程 对象和类
后来学的
在 JavaScript 中,有两种常用的方式访问对象的属性:点表示法和括号表示法。 也可以用来添加对象
-
点表示法
当你写obj.abc123
时,你直接访问对象obj
中名为"abc123"
的属性。 -
括号表示法
当你写obj[prop]
时,prop
是一个变量,其值应该是一个字符串。这个写法会动态地获取对象obj
中键名等于prop
值的属性。
书本内容
let person = {
name:"katsu",
showName(){
console.log(this.name);
}
}
对象的内容是它的属性和行为;那么就其属性而言,js 里有一些内部特性来控制属性的具体性质。
对象 属性的特征
为了将某个特性标识为内部特性,规范会用 两个中括号把特性的名称括起来,比如[[Enumerable]]
。
属性分两种:数据属性和访问器属性。
数据属性
可以把对象的某个属性改成不可修改的:
let person = {};
Object.defineProperty(person, "name", {
writable: false,
value: "Nicholas"
});
console.log(person.name); // "Nicholas"
person.name = "Greg";
console.log(person.name); // "Nicholas"
访问器属性
它们包含一个获取(getter)函数和一个设置(setter)函数,不 过这两个函数不是必需的。
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
合并对象
Object.assign()
Object.assign 会覆盖重复的属性
对象的判定
===
有时候也没用 用 Object. is ()
要检查超过两个值,递归地利用相等性传递即可:
function recursivelyCheckEqual(x, ...rest) {
return Object.is(x, rest[0]) &&
(rest.length < 2 || recursivelyCheckEqual(...rest));
}
方便的设定
有了可计算属性,就可以在对象字面量中完成动态属性赋值。中括号包围的对象属性键告诉运行时 将其作为 JavaScript 表达式而不是字符串来求值:
const jobKey = 'job';
const nameKey = 'name';
const ageKey = 'age';
let person = {
[nameKey]: 'Matt',
[ageKey]: 27,
[jobKey]: 'Software engineer'
};
console.log(person); // { name: 'Matt', age: 27, job: 'Software engineer' }
// 使用对象解构
let person = {
name: 'Matt',
age: 27 };
let { name: personName, age: personAge } = person;
console.log(personName); // Matt
console.log(personAge); // 27
创建对象
类出现之前的东西:
工厂模式是一种众所周知的设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。
构造函数是用于创建特定类型对象的,按照惯例,构造函数名称的首字母都是要大写的,
非构造函数则以小写字母开头。
任何函数只要使用 new 操作符调用就是构造函数,而不使用 new 操 作符调用的函数就是普通函数。
但创建新 Function 实例的机制是一样的。因此不同实例上的函数虽然同名却不相等
原型模式,每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例 共享的属性和方法。
无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype 属性(指向 原型对象)。默认情况下,所有原型对象自动获得一个名为 constructor 的属性,指回与之关联的构 造函数。对前面的例子而言,Person.prototype.constructor 指向 Person。然后,因构造函数而 异,可能会给原型对象添加其他属性和方法。
实例和构造函数原型有联系,和构造函数没有
- prototype 往前指到原型对象- constructor 往后指到构造函数
原型对象*-构造函数(每个构造函数都有原型)
-实例*
Object.create()
使用 delete 删除来自实例的属性
person1.name = "Greg";
console.log(person1.name); // "Greg",来自实例 console.log(person2.name); // "Nicholas",来自原型
delete person1.name;
console.log(person1.name); // "Nicholas",来自原型
枚举和迭代
for-in 循环、Object.keys()、Object.getOwnPropertyNames()、Object.getOwnProperty- 3
Symbols()以及 Object.assign()在属性枚举顺方面有很大区别。for-in 循环和 Object.keys() 的枚举顺是不确定的
Object.values()和 Object.entries()接收一个对象,返回它们内容的数组。Object.values() 返回对象值的数组,Object.entries()返回键/值对的数组。
继承
我自己学的
原型链 查找
原型链查找规则:
function Parent() {}
Parent.prototype.a = 10;
Parent.prototype.b = 20;
function Child() {}
Child.prototype = new Parent();
Child.prototype.b = 30;
Child.prototype.c = 40;
const obj = new Child();
console.log(obj.c); // ??
-
查找
obj.c
:obj
本身没有c
属性 → 进入obj.__proto__
(即Child.prototype
)Child.prototype
有c
,值为40
,✅ 找到,返回40
!
-
查找
obj.b
:obj
本身没有b
→ 进入Child.prototype
Child.prototype.b = 30
,✅ 找到,返回30
!- 即使
Parent.prototype
里有b = 20
,但它不会被访问到,因为Child.prototype
里已经有b = 30
了!
原型链 挂载
constructor 是 prototype 下面的一个属性所以需要手动定义 否则是空的 为了链完整创建完对象以后要手动链回自身
function Person(name) {
this.name = name;
}
Person.prototype.getName = function () {
return this.name;
};
function Student(name, age) {
Person.call(this, name); // 调用 Person 构造函数
this.age = age;
}
// 继承 Person
Student.prototype = Object.create(Person.prototype);
//- **创建一个新对象**,它的 `__proto__` 指向 `Person.prototype`
// 但是,这个新对象是一个干净的空对象,没有 `constructor`!
Student.prototype.constructor = Student;
// 这句不写的话Student.prototype.constructor =Person
const child = new Student("Alice", 20);
console.log(child.getName()); // ??
原型链
原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函 数。这样就在实例和原型之间构造了一条原型链。
实现继承的关键,是 SubType 没有使用默认原型,而是将其替换成了一个新的对象。这个 新的对象恰好是 SuperType 的实例。
原型中包含的引用值会在所有实例间共享,这也是为什么属性通常会 在构造函数中定义而不会定义在原型上的原因。
最终结果是,SubType 的所有实例都会 共享这个 colors 属性。这一点通过 instance1.colors 上的修改也能反映到 instance2.colors 上就可以看出来。
“盗用构造函数”(constructor stealing)的技 术在开发社区流行起来(这种技术有时也称作“对象伪装”或“经典继承”)。基本思路很简单:在子类 构造函数中调用父类构造函数。
组合继承(有时候也叫伪经典继承)综合了原型链和盗用构造函数,将两者的优点集中了起来。基 本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方 法定义在原型上以实现重用,又可以让每个实例都有自己的属性。
组合继承弥补了原型链和盗用构造函数的不足,是 JavaScript 中使用最多的继承模式。而且组合继 承也保留了 instanceof 操作符和 isPrototypeOf()方法识别合成对象的能力。
更优化:寄生式组合继承
类
我自己学到的
在类中写的方法(非 static
)都会自动挂载到 prototype
上,所以"实例方法"和"原型方法"是一样的概念!
✔ 类属性(静态属性)只能通过 类
调用,而实例属性只能通过 实例
调用!
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(`Hello, my name is ${this.name}`);
}
}
const person = new Person("Alice");
person.sayHello(); // Hello, my name is Alice
定义类 声明或者表达式
class Person{};
const Animal = class {};
函数受函数作用域限制,而类受块作用域限制
构造函数
class Animal {}
class Person {
constructor() {
console.log('person ctor');
}
}
class Vegetable {
constructor() {
let p = new Person(); // person ctor 8
this.color = 'orange';
}
}
let a = new Animal();
let v = new Vegetable();
console.log(v.color); // orange
类构造函数会在执行之后返回 this 对象。构造函数返回的对象会被用作实例化的对 象,如果没有什么引用新创建的 this 对象,那么这个对象会被销毁
把类当成特殊函数
类是 JavaScript 的一等公民,因此可以像其他对象或函数引用一样把类作为参数传递
类本身具有与通构造函数一样的行为。在类的上下文中,类本身在使用 new 调用时就 会被当成构造函数。重点在于,类中定义的 constructor 方法不会被当成构造函数,在对它使用 instanceof操作符时会返回false。但是,如果在创建实例时直接将类构造函数当成通构造函数来 使用,那么 instanceof 操作符的返回值会反转
定义哪个属于啥 属于类还是原型还是实例
constructor 里面的不共享、
静态类方法非常适合作为实例工厂:
class Person {
constructor(age) {
this.age_ = age;
}
sayAge() {
console.log(this.age_);
}
static create() {
// 使用随机年龄创建并返回一个 Person 实例
return new Person(Math.floor(Math.random()*100));
} }
console.log(Person.create()); // Person { age_: ... }
- 继承基础
ES6 类支持单继承。使用 extends 关键字,就可以继承任何拥有[[Construct]]和原型的对象。