js红书 【2 对象和类】

katsu 发布于 2025-02-17 119 次阅读


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 中,有两种常用的方式访问对象的属性点表示法括号表示法。 也可以用来添加对象

  1. 点表示法
    当你写 obj.abc123 时,你直接访问对象 obj 中名为 "abc123" 的属性。

  2. 括号表示法
    当你写 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。然后,因构造函数而 异,可能会给原型对象添加其他属性和方法。

实例和构造函数原型有联系,和构造函数没有

  1. 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); // ??
  1. 查找 obj.c

    • obj 本身没有 c 属性 → 进入 obj.__proto__(即 Child.prototype
    • Child.prototype c,值为 40,✅ 找到,返回 40
  2. 查找 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_: ... }
  1. 继承基础

ES6 类支持单继承。使用 extends 关键字,就可以继承任何拥有[[Construct]]和原型的对象。