Skip to content

对象

对象是一组属性的无序集合,通常使用对象字面量来创建对象:

javascript
let person = {
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",
  sayName() {
    console.log(this.name);
  }
}

对象属性的类型

这里自己的理解数据属性和访问器属性的设计,为我们提供了使用js对象的灵活性,我们可以有两种方式定义对象的属性,当我们直接使用对象字面量的方式创建对象,对象的属性是数据属性,也可以定义访问器属性来增强js对象。

属性分为两种:数据属性和访问器属性。

数据属性:数据属性包含一个保存数据值的位置。数据属性有以下几个内部特性:

  • [[Configurable]] 默认为true,表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。
  • [[Enumerable]] 默认为true,表示属性是否可以通过for-in循环返回。
  • [[Writable]] 默认为true,表示属性的值是否可以被修改
  • [[Value]]包含属性实际的值。

访问器属性

访问器属性不包含数据值,而是包含一个获取(get)函数和设置(set)函数。访问器属性有以下几个内部特性

  • [[Configurable]]
  • [[Enumerable]]
  • [[Get]] 获取函数
  • [[Set]] 设置函数

访问器属性通常用于这种情形:当我们有一些属性不希望直接被外部访问,或设置对象的一个属性值会导致一些其他的变化。

javascript
// 定义一个对象,包含伪私有成员year_和公共成员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

不管是数据属性还是访问器属性,我们都需要使用Object.defineProperty方法。

下边这张图,当我们给name属性定义访问器属性后,person对象的name属性就变为了一个访问器属性,age还是一个数据属性。

****

创建对象

ES6之前js是没有class关键字的(ES6也只是原型的语法糖),当我们创建某种类型的对象时,通常是创建自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。(其实是利用js 函数的特性的一种取巧的办法)

构造函数本身也是函数,js中没有特定定义构造函数的语法,任何函数使用new 操作符就是构造函数。

javascript
function Person() {
  this.name = "Jake";
  this.sayName = function() {
    console.log(this.name);
  };
}
// 作为构造函数
let person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); // "Nicholas"
// 作为函数调用
Person("Greg", 27, "Doctor"); // 添加到window对象
window.sayName(); // "Greg"
// 在另一个对象的作用域中调用
let o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); // "Kristen"

原型

原型是js实现类、继承等最重要的因素。

JavaScript 1.1 不再需要直接在每个新实例上创建方法属性。它通过函数对象名为 prototype 的属性,将原型对象与构造函数关联起来。《JavaScript 1.1 指南》[Netscape 1996e] 将 prototype 描述为「由所有该类型对象共享的属性」。这是个模糊的描述,更好的表述可能是这样的:原型是一种特殊的对象,其自身属性与所有「由构造函数创建的对象」所共享。

对这种共享机制没有更进一步的说明,但可以发现原型对象具备如下特征:

访问对象属性时,如果这个属性的名称在「与对象构造函数相关联的原型」上已被定义,那么将返回原型对象的属性值。

对原型对象属性的添加或修改,对于通过「与原型相关联的构造函数」创建的现有对象,是立即可见的。

为对象属性赋值时,会遮盖g18在「与对象构造函数相关联的原型」上定义的同名属性值。

3.1 基础

prototype

每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型

原型对象默认会有constructor属性,指向与之关联的构造函数。

下边这张图说明了Person构造函数、Person的原型对象和Person现有两个实例之间的关系。

  • Person函数的原型对象Person.prototype包含constructor属性和其他后来添加的属性。
  • Person.prototype指向原型对象,而Person.prototype.contructor指回Person构造函数。
  • <font style="color:rgb(0,0,0);">Person</font><font style="color:rgb(0,0,0);">Person.prototype</font>Person

proto

既然有了prototype,那__proto__又是什么呢,为什么会出现?

__proto__属性之前不是js标准,但由于各大浏览器厂商都已经实现了,才被加入了ES6。

在JS中,任何对象都有__proto__属性,__proto__可被称为隐式原型(区别于prototype),指向构造该对象的构造函数的原型

也就是实例的__proto__属性指向实例构造函数的原型。

我们在浏览器调试时,经常会看到层层嵌套的__proto__属性,这是因为比如Person的实例person1,person1__proto__属性,

person1.__proto__也是一个对象,也有__proto__对象,这样层层嵌套,直到__proto__属性是一个object类型

我们通过下边这张图来看一下:

  • Person函数有prototype原型属性,同时Person函数也是一个funtion类型的对象,有__proto__隐式原型属性。
  • Person是一个函数,也是一个**function**类型的对象,__proto__隐式原型属性指向的function 的构造函数。
  • person1实例是一个对象,只有__proto__隐式原型属性。指向Person prototype

画板

prototype 与 __proto__的一些总结

  • 对象具有__proto__prototype属性,而函数只有prototype
  • 对象的__proto__属性指向-->构造该对象的构造函数的原型

https://www.zhihu.com/question/34183746/answer/124279182?utm_psn=1836143786167783424

3借助原型特性模拟"类"行为

原型可以解决对象之间共享属性和方法的问题,也就是继承,但直接使用原型实现继承的话,会带来一个问题:对于方法的继承来说是可以的,但对于属性为引用值来说,多个实例之间共享同一个引用值属性,一个实例修改引用值属性会造成其他实例也会受到影响。

因为上述的一些问题,开发者们想出了很多办法,利用原型的特性实现继承:

  • 开发者们想出来通过****

es6引入了class关键字,但本质上只是语法糖,背后仍是对原型的封装。

这并不意味着使用 class 语法没有价值,因为class 语法提供了更高的可读性和清晰度。

类定义

javascript
class Person {
  constructor(name) {
    this.name = name
  },
  getName(){
    return this.name
  }
}

数Constructor

constructor关键字用于在类定义块内部创建类的构造函数,一个类必须有construct方法,如果没有显示定义,解释器会默认添加一个空的 construct。

``

当我们

  1. 创建的这个新对象内部的[[Prototype]]指针被赋值为constructor构造函数的prototype属性(其实就是这个类的原型对象)。
  2. 构造函数内部的this被赋值为这个新对象(即this指向新对象)。
  3. 执行构造函数内部的代码(给新对象添加属性)。
  4. 如果构造函数返回非空对象,则返回该对象 this;否则,返回刚创建的新对象。

类实例化时,传入的参数会用作构造函数的参数。

javascript
class Person {
  constructor(name) {
    console.log(arguments.length);
    this.name = name || null;
  }
}
let p1 = new Person;             // 0
console.log(p1.name);           // null

类是一种特殊的函数

ECMAScript中没有正式的类这个类型。从各方面来看,ECMAScript类就是一种特殊函数。声明一个类之后,通过typeof操作符检测类标识符,表明它是一个函数:

javascript
class Person {}
console.log(Person);           // class Person {}
console.log(typeof Person);   // function

实例、原型和类成员

这可能是 class 语法最大的作用, class 的语法可以非常方便地定义应该存在于实例上的成员、应该存在于原型上的成员,以及应该存在于类本身的成员

理清这几个概念很重要:

  • 实例成员:通过 new 操作符创建的类对象实例(this) 上的成员,由实例独享
  • 原型成员:实例的原型上的成员,会被所有实例共享
  • 类本身成员:相当于 static 静态成员 ,js 中类本身是一个函数对象,也就是这个函数对象的成员

实例成员

es6 中,添加实例成员必须在类构造函数中,为新创建的实例(this)添加“自有”属性

javascript
class Person {
  constructor() {
    this.name = new String('Jack');
  }
}

ES2022 为类的实例属性,又规定了一种新写法,可以定义在类内部的最顶层:

javascript
class IncreasingCounter {
  _count = 0;
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}

原型方法和访问器

类定义语法把在类块中定义的方法作为原型方法,也就是定义在类的prototype属性上。

javascript
class Person {
  constructor() {
    // 添加到this的所有内容都会存在于不同的实例上
    this.locate = () => console.log('instance');
  }
  // 在类块中定义的所有内容都会定义在类的原型上
  locate() {
    console.log('prototype');
  }
}
let p = new Person();
p.locate();                     // instance
Person.prototype.locate();   // prototype

类定义也支持获取和设置访问器。语法与行为跟普通对象一样:

javascript
class Person {
  set name(newName) {
    this.name_ = newName;
  }
  get name() {
    return this.name_;
  }
}
let p = new Person();
p.name = 'Jake';
console.log(p.name); // Jake

静态类方法

静态类成员在类定义中使用static关键字作为前缀。在静态成员中,this引用类自身。其他所有约定跟原型成员一样:

javascript
class Person {
  constructor() {
    // 添加到this的所有内容都会存在于不同的实例上
    this.locate = () => console.log('instance', this);
  }
  // 定义在类的原型对象上
  locate() {
    console.log('prototype', this);
  }
  //定义在类本身上
  static locate(){
    console.log('class', this);
  }
}
let p = new Person();
p.locate();                     // instance, Person {}
Person.prototype.locate();   // prototype, {constructor: ... }
Person.locate();               // class, class Person {}

继承

Class 可以通过extends实现继承,让子类继承父类的属性和方法。extends 的写法比 ES5 的原型链继承,要清晰和方便很多。

几个知识点:

  • super

super

super 关键字指代父类,这个关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。

super()调用父类的construct方法,也就是创建父类的实例。

javascript
class Point { /* ... */ }

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

ES6 规定,子类必须在constructor()方法中调用super(),否则就会报错。

为什么子类的构造函数,一定要调用super()?原因就在于 ES6 的继承机制,与 ES5 完全不同。

  • ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后”。
  • ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”。这就是为什么 ES6 的继承必须先调用super()方法,因为这一步会生成一个继承父类的this对象,没有这一步就无法继承父类。

私有属性和私有方法的继承

子类无法继承父类的私有属性,或者说,私有属性只能在定义它的 class 里面使用。

如果父类定义了私有属性的读写方法,子类就可以通过这些方法,读写私有属性。

javascript
class Foo {
  #p = 1;
  getP() {
    return this.#p;
  }
}

class Bar extends Foo {
  constructor() {
    super();
    console.log(this.getP()); // 1
  }
}

静态属性和静态方法的继承

待回答问题

  • class 实现继承后的原型链;