# 对象

ECMA-262 把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。” 对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。每个对象都是基于一个引用类型创建的

对象创建方法可以有:

使用 new 操作符后跟一个构造函数来创建:

var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";

person.sayName = function() {
  alert(this.name);
};

使用对象字面量创建:

var person = {
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",

  sayName: function() {
    alert(this.name);
  },
};

# 属性类型

JS 定义只有内部才用的特性(attribute),描述了属性(property)的各种特征。(特性用来描述属性) 这些特性是为了实现 JavaScript 引擎用的,因此在 JavaScript 中不能直接访问它们。为了表示特性是内部值,特性放在两对儿方括号中 [[]],例如 [[Enumerable]]

ECMAScript 中有两种属性:数据属性 和 访问器属性。

# 数据属性

数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有 4 个描述其行为的特性。

  • [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true
  • [[Enumerable]]:表示能否通过 for-in 循环返回 (遍历) 属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true
  • [[Writable]]:表示能否修改属性的值。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true
  • [[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为 undefined

要修改属性默认的特性,必须使用 Object.defineProperty() 方法. 这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符(descriptor)对象的属性必须是:configurableenumerablewritablevalue。设置其中的一或多个值,可以修改对应的特性值。

var person = {};
person.name = "Greg";
alert(person.name); //"Greg"

// name 属性值改成 "Nicholas", 并且设置为不可修改属性值
Object.defineProperty(person, "name", {
  writable: false,
  value: "Nicholas",
});

alert(person.name); //"Nicholas"
person.name = "Greg";
alert(person.name); //"Nicholas"
var person = {};
Object.defineProperty(person, "name", {
  configurable: false,
  value: "Nicholas",
});

//抛出错误
Object.defineProperty(person, "name", {
  configurable: true,
  value: "Nicholas",
});

一旦把属性定义为不可配置 configurable: false 的,就不能再把它变回可配置了。此时,再调用 Object.defineProperty() 方法修改除 writable 之外的特性,都会导致错误.

在调用Object.defineProperty()方法时,如果不指定,configurableenumerablewritable 特性的默认值都是 false

# 访问器属性

访问器属性不包含数据值;它们包含一对 gettersetter 函数(不是必需的)

  • 在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;
  • 在写入访问器属性时,会调用 setter 函数并传入新值,这个函数负责决定如何处理数据。

访问器属性有如下 4 个特性。

  • [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性的默认值为 true
  • [[Enumerable]]:表示能否通过 for-in 循环返回属性。对于直接在对象上定义的属性,这个特性的默认值为 true
  • [[Get]]:在读取属性时调用的函数。默认值为 undefined
  • [[Set]]:在写入属性时调用的函数。默认值为 undefined

访问器属性不能直接定义,必须使用 Object.defineProperty() 来定义。

var book = {
  // 加 下划线 ("_") 表示只能通过对象方法访问的属性
  _year: 2004,
  edition: 1,
};

Object.defineProperty(book, "year", {
  get: function() {
    return this._year;
  },
  set: function(newValue) {
    if (newValue > 2004) {
      this._year = newValue;
      this.edition += newValue - 2004;
    }
  },
});

book.year = 2005;
alert(book.edition); //2

使用访问器属性,设置一个属性的值会导致其他属性发生变化。

只指定 getter 意味着属性是不能写,尝试写入属性会被忽略。在严格模式下,尝试写入只指定了 getter 函数的属性会抛出错误。类似地,只指定 setter 函数的属性也不能读,否则在非严格模式下会返回 undefined,而在严格模式下会抛出错误。

# 特性相关其他操作

# 修改多个属性的特性

使用 Object.defineProperties() 方法, 可以通过一个描述符定义多个属性的特性 方法接收两个对象参数:

  • 第一个对象是要添加和修改其属性的对象
  • 第二个对象的属性与第一个对象中要添加或修改的属性一一对应
var book = {};

// 代码在 book 对象上定义了两个数据属性(_year和edition)和一个访问器属性(year)
Object.defineProperties(book, {
  _year: {
    writable: true,
    value: 2004,
  },

  edition: {
    writable: true,
    value: 1,
  },

  year: {
    get: function() {
      return this._year;
    },

    set: function(newValue) {
      if (newValue > 2004) {
        this._year = newValue;
        this.edition += newValue - 2004;
      }
    },
  },
});

# 读取属性的特性

Object.getOwnPropertyDescriptor() 方法,可以取得给定属性的描述符, 方法接收两个参数:属性所在的对象 和 要读取其描述符的属性名称。

var book = {};

Object.defineProperties(book, {
  _year: {
    value: 2004,
  },

  edition: {
    value: 1,
  },

  year: {
    get: function() {
      return this._year;
    },

    set: function(newValue) {
      if (newValue > 2004) {
        this._year = newValue;
        this.edition += newValue - 2004;
      }
    },
  },
});

var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
alert(descriptor.value); //2004
alert(descriptor.configurable); //false
alert(typeof descriptor.get); //"undefined"

var descriptor = Object.getOwnPropertyDescriptor(book, "year");
alert(descriptor.value); //undefined
alert(descriptor.enumerable); //false
alert(typeof descriptor.get); //"function"

# 属性检测

在开发中, 我们需要判断属性与对象的所属关系. 检测方法有如下几种:

# in 运算符

in 操作符, 左边是属性名(字符串), 右边是对象. 如果对象的自有属性或继承属性中包含这个属性, 则返回 true

var obj = {
  a: 123,
};

"a" in obj; // true 自有属性
"toString" in obj; // true 继承属性

# hasOwnProperty()

hasOwnProperty() 可以用来判断属性是否为对象自有属性. 参数为属性名的字符串形式.

var obj = {
  a: 123,
};

obj.hasOwnProperty("a"); // true

# propertyIsEnumerable()

propertyIsEnumerable() 可以算是 hasOwnProperty() 的加强版, 它只有当属性为自有属性, 且可枚举时, 才返回 true

# 属性枚举

# for/in 循环

for/in 循环用于遍历对象中所有可枚举的属性. 在循环中, 遍历出的属性名称被赋值给循环变量.

var o = {
  a: 1,
  b: 2,
  c: 3,
};

for (prop in o) {
  console.log(prop); // 依次输出 a, b, c
}

# Object.keys()

Object.keys() 返回一个数组, 数组由对象的 "可枚举的自有属性的名称" 组成 (不含 Symbol 属性).

操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用 for...in 循环,而用 Object.keys() 代替。

# Object.getOwnPropertyNames()

Object.getOwnPropertyNames() 返回一个数组, 数组由对象的 "自有属性名称" 组成 (不含 Symbol 属性), 而不仅仅是可枚举的.

# Object.getOwnPropertySymbols()

Object.getOwnPropertySymbols() 返回一个数组,包含对象 "自有" 的所有 Symbol 属性的键名。

# Reflect.ownKeys()

Reflect.ownKeys() 返回一个数组,包含对象 "自身" 的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

# 对象创建

# 工厂模式

为了抽象了创建具体对象的过程, 开发人员  发明了一种函数用来封装以特定接口创建对象的细节

function createPerson(name, age, job) {
  var o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function() {
    alert(this.name);
  };
  return o;
}

var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");

工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)

# 构造函数模式

构造函数可用来创建特定类型的对象, 例如,可以使用构造函数模式将前面的例子重写如下。

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function() {
    alert(this.name);
  };
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

构造函数 和 工厂模式 相比:

  • 没有显式地创建对象;
  • 直接将属性和方法赋给了 this 对象;
  • 没有 return 语句。

用构造函数创建新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4 个步骤:

  • 创建一个新对象;
  • 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
  • 执行构造函数中的代码(为这个新对象添加属性);
  • 返回新对象。

用构造函数创建的  对象实例, 有一个 constructor 属性, 指向构造函数:

前面的例子里:

alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true

instanceof 操作符, 可以用来检测对象类型:

例子中创建的所有对象既是 Object 的实例,同时也是 Person 的实例. 所有对象均继承自 Object

alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true

# 将构造函数当作函数

构造函数与其他函数的唯一区别,就在于调用它们的方式不同. 任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过 new 操作符来调用,那它跟普通函数也不会有什么两样。

// 当作构造函数使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); //"Nicholas"

// 作为普通函数调用
Person("Greg", 27, "Doctor"); // 添加到window
window.sayName(); //"Greg"

// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //"Kristen"

# 构造函数的问题

使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。

例子中,person1person2 都有一个名为 sayName() 的方法,但那两个方法不是同一个 Function 的实例。 这样不同实例上的同名函数是不相等的.

alert(person1.sayName == person2.sayName); //false

通过把函数定义转移到构造函数外部来解决这个问题:

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = sayName;
}

function sayName() {
  alert(this.name);
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

但是这样的话, 在外部定义的方法就成了全局函数, 自定义的引用类型丝毫没有封装性. 这些问题可以通过使用原型模式来解决。

# 原型模式

创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

一般来说开发中我们希望  每个实例都有自己的实例属性, 但是共享方法. 所以开发中一般把属性放在构造函数, 把共享的属性和方法放在原型对象中.

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.friends = ["Shelby", "Court"];
}

Person.prototype.sayName = function() {
  alert(this.name);
};

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true

# 理解原型对象

原型对象默认有一个 constructor(构造函数)属性,这个属性是一个指向原型对象所属构造函数指针.

当调用构造函数创建一个新实例后,该实例的内部将包含一个指针 [[Prototype]](内部属性),指向构造函数的原型对象。在很多浏览器上, 对象中的 __proto__ 属性指向原型对象。

09.d06z.01

可以通过 isPrototypeOf() 方法来确定对象之间是否存在原型与实例的关系. 从本质上讲,如果实例对象的 [[Prototype]] 指向调用 isPrototypeOf() 方法的对象,那么这个方法就返回 true

Object.getPrototypeOf() 可以返回 [[Prototype]] 指向的原型对象.

# 对象字面量重写原型对象

可以用一个包含所有属性和方法的对象字面量来重写整个原型对象

function Person() {}

Person.prototype = {
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",
  sayName: function() {
    alert(this.name);
  },
};

前面说 原型对象的 constructor 属性指向  原型对象所在构造函数. 使用对象字面量重写原型对象, constructor 属性也就被改写了.

可以手动设置 constructor 属性的值.

function Person() {}

Person.prototype = {
  constructor: Person,
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",
  sayName: function() {
    alert(this.name);
  },
};

注意,以这种方式重设 constructor 属性会导致它的 [[Enumerable]] 特性被设置为 true。默认情况下,原生的 constructor 属性是不可枚举的

这时可以用 Object.defineProperty() 方法来  设置 constructor

Object.defineProperty(Person.prototype, "constructor", {
  enumerable: false,
  value: Person,
});

# 检测属性  存在位置

使用 hasOwnProperty() 方法可以检测一个属性是存在于实例中,还是存在于原型中。只在给定属性存在于对象实例中时,才会返回 true

function Person() {}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
  alert(this.name);
};

var person1 = new Person();
var person2 = new Person();

alert(person1.hasOwnProperty("name")); //false

person1.name = "Greg";
alert(person1.name); //"Greg"——来自实例
alert(person1.hasOwnProperty("name")); //true

alert(person2.name); //"Nicholas"——来自原型
alert(person2.hasOwnProperty("name")); //false

delete person1.name;
alert(person1.name); //"Nicholas"——来自原型
alert(person1.hasOwnProperty("name")); //false

可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。

# 把原型  对象封装在  构造函数中

在  构造函数外定义原型  对象, 可能还是缺乏封装性. 可以改成通过在构造函数中初始化原型. 在  初次调用构造函数的时候初始化原型.

function Person(name, age, job) {
  //属性
  this.name = name;
  this.age = age;
  this.job = job;

  //方法
  if (typeof this.sayName != "function") {
    Person.prototype.sayName = function() {
      alert(this.name);
    };
  }
}

var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();

上面例子中, 只在 sayName() 方法不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化

# Class

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过 class 关键字,可以定义类。

// ES5 原型模式
function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function() {
  return "(" + this.x + ", " + this.y + ")";
};

var p = new Point(1, 2);

上面代码用 class 改写的话为:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return "(" + this.x + ", " + this.y + ")";
  }
}

var p = new Point(1, 2);

constructor 方法,这就是构造方法,而 this 关键字则代表实例对象。

定义 “类” 的方法的时候,前面不需要加上 function 这个关键字,直接把函数定义放进去了就可以了。另外,方法之间 "不需要逗号分隔",加了会报错。

创建实例的时候,也是直接对 "类" 使用 new 命令,跟构造函数的用法完全一致。

class Point {
  // ...
}

typeof Point; // "function"
Point === Point.prototype.constructor; // true

上面代码表明, 其实 ES6 的类,完全可以看作构造函数的另一种写法。类的数据类型就是函数,类本身就指向构造函数。类的所有方法也都定义在类的 prototype 属性上面。

# 注意点

与 ES5 不同的是, 类的内部所有定义的方法,都是不可枚举的. 这一点与 ES5 的行为不一致。

类不存在变量提升(hoist),这一点与 ES5 完全不同。

new Foo(); // ReferenceError
class Foo {}

# 静态方法 & 静态属性

所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为 “静态方法”。

class Foo {
  static classMethod() {
    return "hello";
  }
}

Foo.classMethod(); // 'hello'

var foo = new Foo();
foo.classMethod();
// TypeError: foo.classMethod is not a function

注意,如果静态方法包含 this 关键字,这个 this 指的是类,而不是实例。

"静态属性" 指的是 Class 本身的属性,而不是定义在实例对象上的属性。

class Foo {}

Foo.prop = 1;
Foo.prop; // 1

上面的写法为 Foo 类定义了一个静态属性 prop

现在有一个提案提供了类的静态属性,写法是在实例属性法的前面,加上 static 关键字。

class MyClass {
  static myStaticProp = 42;

  constructor() {
    console.log(MyClass.myStaticProp); // 42
  }
}

# 私有方法 & 私有属性

私有方法和私有属性,是 "只能在类的内部访问的方法和属性",外部不能访问。这是常见需求,有利于代码的封装,但 ES6 不提供,只能通过变通方法模拟实现。

❗️❗️❗️ 先不写

# 继承

简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

# 原型链

原型链是实现继承的主要方法, 基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法.

假如让  一个  类型的原型对象等于另一个类型的实例, 此时这个类型的原型对象将包含一个指向另一个原型的指针. 像这样层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。

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;
};

var instance = new SubType();

alert(instance.getSuperValue()); //true

这个例子中的实例以及构造函数和原型之间的关系如下图:

09.d06z.05

# 确定原型和实例的关系

可以通过两种方式来确定原型和实例之间的关系。第一种方式是使用 instanceof 操作符:

只要操作符前面实例对象的原型链中, 存在操作符后面  类型的实例对象, 就返回 true:

alert(instance instanceof Object); //true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true

第二种方式是使用 isPrototypeOf() 方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此 isPrototypeOf() 方法也会返回 true

alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true

#  重写添加超类型中的方法

给原型添加方法的代码一定要放在替换原型的语句之后

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;
};

//重写超类型中的方法
SubType.prototype.getSuperValue = function() {
  return false;
};

var instance = new SubType();
alert(instance.getSuperValue()); //false

# 原型链的问题

第一个问题是, 因为原型  中的属性会被其实例  共享. 在通过原型来实现继承时,实例实际上会变成另一个类型的原型。原先的实例属性也就顺理成章地变成了现在的原型属性了。

function SuperType() {
  this.colors = ["red", "blue", "green"];
}

function SubType() {}

//继承了SuperType
SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green,black"

第二个问题是, 在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。

# 借用构造函数

借用构造函数, 也叫做伪造对象或经典继承, 思想是在子类型构造函数的内部调用超类型构造函数. 通过使用 apply()call() 方法可以在(将来)新创建的对象上执行构造函数.

前面说 this 指向函数调用时所在的执行环境, 或者说调用这个函数的对象. 在创建子类型的实例的时候 call() 方法, 让超类型的 this 指向新实例.

function SuperType() {
  this.colors = ["red", "blue", "green"];
}

function SubType() {
  //继承了SuperType
  SuperType.call(this);
}

var instance1 = new SubType();

instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green"

通过使用 call() 方法(或 apply() 方法也可以),我们实际上是在(未来将要)新创建的 SubType 实例的环境下调用了 SuperType 构造函数。这样一来,就会在新 SubType 对象上执行 SuperType() 函数中定义的所有对象初始化代码。结果, SubType 的每个实例就都会具有自己的 colors 属性的副本了。

# 传递参数

通过街用构造函数, 可以在子类型构造函数中向超类型构造函数传递参数

function SuperType(name) {
  this.name = name;
}

function SubType() {
  //继承了SuperType,同时还传递了参数
  SuperType.call(this, "Nicholas");

  //实例属性
  this.age = 29;
}

var instance = new SubType();
alert(instance.name); //"Nicholas";
alert(instance.age); //29

# 借用构造函数的问题

但是这样的话, 继承来的方法都在构造函数中定义,因此函数复用就无从谈起了。

# 组合继承

组合继承, 也叫做伪经典继承, 指的是将原型链和借用构造函数的技术组合到一块. 思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function() {
  alert(this.name);
};

function SubType(name, age) {
  //继承属性
  // 因为在这里传递参数, SubType 原型中的 SuperType 属性会被覆盖
  SuperType.call(this, name);

  this.age = age;
}

//继承方法
SubType.prototype = new SuperType();

// 因为 SubType 原型是 SuperType 实例, 所以 原型的 construtor 指向的是超类型, 要手动设置.
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
  alert(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29

var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

这种是 JavaScript 中最常用的继承模式.

# extends 继承

Class 可以通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

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

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

let cp = new ColorPoint(25, 8, "green");

cp instanceof ColorPoint; // true
cp instanceof Point; // true

# super

super 关键字作为函数调用时,表示父类的构造函数,用来新建父类的 this 对象。

子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,在此基础加上子类自己的实例属性和方法。

上面代码中 super() 相当于 Point.prototype.constructor.call(this)

注意: 父类的静态方法,也会被子类继承

# Object.getPrototypeOf()

Object.getPrototypeOf 方法可以用来从子类上获取父类。

Object.getPrototypeOf(ColorPoint) === Point;
// true

因此,可以使用这个方法判断,一个类是否继承了另一个类。

# 类的 prototype 属性 & proto 属性

前面说过, ES5 实现之中,每一个实例都有 __proto__ 属性,指向对应的构造函数的 prototype 属性。Class 作为构造函数的语法糖,同时有 prototype 属性和 __proto__ 属性

  • 子类的 __proto__ 属性,表示构造函数的继承,总是指向父类。
  • 子类的 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的 prototype 属性。
class A {}

class B extends A {}

B.__proto__ === A; // true
B.prototype.__proto__ === A.prototype; // true

可以这样理解:作为一个对象,子类(B)的原型(__proto__ 属性)是父类(A);作为一个构造函数,子类(B)的原型对象(prototype 属性)是父类的原型对象(prototype 属性)的实例。

子类实例的 __proto__ 属性的 __proto__ 属性,指向父类实例的 __proto__ 属性。也就是说,子类的原型的原型,是父类的原型。

var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, "red");

p2.__proto__ === p1.__proto__; // false
p2.__proto__.__proto__ === p1.__proto__; // true

# 序列化对象

"对象序列化" 指的是将对象的状态转换为字符串, 或将字符串转换为对象.

ES5 提供内置函数 JSON.stringify()JSON.parse() 来序列化和还原 JavaScript 对象.

这里不做更多讲解.

MDN JSON 参考

上次更新: 9/13/2020, 9:33:17 AM