红宝书学习:第八章 对象,类,面向对象

本章内容

  • 理解对象:属性,内部特性,解构赋值
  • 理解对象创建过程:字面量,工厂,构造函数,原型
  • 理解继承:原型链,盗用构造函数,组合式,组合式寄生
  • 理解类:构造函数,实例成员,原型方法,静态类方法,继承

对象

JavaScript的对象是一组由键-值组成的无序集合

  • 对象的键都是字符串类型,值可以是任意数据类型。

  • 其中每个键又称为对象的属性,要获取一个对象的属性,用对象变量.属性名

创建对象2种方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. new Object(),然后手动添加属性,现在不怎么用了
let person=new Object()
person.name="a"
person.sayName=function(){
console.log(this.name)
}

// 2. 使用对象字面量,现在都用这个,注意多个属性用逗号分隔
let name=1
let person={
[name]:"a",
sayName(){
console.log(this.name)
}
}

属性的类型和特性

内部特性/属性描述符

  • 用来描述属性的特征,开发者不能在JS中直接访问,用双中括号[[]]表示
  • 属性分为数据属性和访问器属性,有不同的内部特性

数据属性:p349

  • 包含一个保存数据值的位置。

  • 4个特性

    • [[Configurable]] 是否可delete,是否可修改特性,是否可以改成访问器属性
    • [[Enumerable]] 是否可以通过for-in返回(遍历对象属性)
    • [[Writable]] 是否可修改
    • [[Value]] 值
  • 使用Object.defineProperty()方法修改

访问器属性:p351

  • 没有值,但是有一个getter函数或一个setter函数,可用于私有成员 p351
  • https://www.w3school.com.cn/js/js_object_accessors.asp
  • 4个特性
    • [[Configurable]] 是否可delete,是否可修改特性,是否可改为数据属性
    • [[Enumerable]] 是否可以通过for-in循环返回
    • [[Get]] 获取函数,在读取属性时调用
    • [[Set]] 设置函数,在写入属性时调用
  • 使用Object.defineProperty()方法修改,也可以直接定义set和get
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 直接定义set get 对象访问器
var person = {
language_ : "zh", // 属性名_一般代表不想被直接访问的属性
get langCap() {
return this.language_.toUpperCase()
},
set lang(val) {
this.language_=val
}
};

console.log(person.langCap) // ZH
person.lang="en"
console.log(person.langCap) // EN

定义多个属性

Object.defineProperty(obj, prop)一次只能定义一个属性的特性

Object.defineProperties(obj, props)方法可以一次定义多个属性及其特性 p352

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var person = {
language_:"zh"
}
Object.defineProperties(person, {
langCap : {
get : function() {
return this.language_.toUpperCase()
}
},
lang :{
set : function(val){
this.language_=val
}
}
})

console.log(person.langCap) // ZH
person.lang="en"
console.log(person.langCap) // EN

读取属性的特性

Object.getOwnPropertyDescriptor(obj,prop)取得指定属性的属性描述符

Object.getOwnPropertyDescriptors(obj)ES2017新增,返回对象的说有属性及其描述符

合并对象 *

合并:把源对象所有的本地属性一起复制到目标对象上

Object.assign(dest,src)方法,源对象复制到目标对象,返回目标对象

  • 本质上是执行

  • 执行浅拷贝:p356

    • 可以有多个src,如果src之间有相同的属性,那么最终dest的那个属性使用最后一个复制的值
    • 浅拷贝复制的是对象的引用,仍然指向同一个地址,所以如果属性值是对象,拷贝后修改,源和目的都会改变。
    • 另,如果src中有getter函数会报错,p357。要复制getter用Object.getOwnPropertyDescriptor(obj,prop)Object.defineProperty(obj, prop)

对象标识及相等判定 *

Object.is()方法,ES6新增

===无法判定的情况:

1
2
3
4
5
6
7
8
9
10
11
// 这些是===符合预期的情况
console.log(true === 1); // false
console.log({} === {}); // false
console.log("2" === 2); // false
// 这些情况在不同JavaScript引擎中表现不同,但仍被认为相等
console.log(+0 === -0); // true
console.log(+0 === 0); // true
console.log(-0 === 0); // true
// 要确定NaN的相等性,必须使用极为讨厌的isNaN()
console.log(NaN === NaN); // false
console.log(isNaN(NaN)); // true

Object.is()

1
2
3
4
5
6
7
8
9
console.log(Object.is(true, 1)); // false
console.log(Object.is({}, {})); // false
console.log(Object.is("2", 2)); // false
// 正确的0、-0、+0相等/不等判定
console.log(Object.is(+0, -0)); // false
console.log(Object.is(+0, 0)); // true
console.log(Object.is(-0, 0)); // false
// 正确的NaN相等判定
console.log(Object.is(NaN, NaN)); // true

检查多个值,递归调用

1
2
3
4
function recursivelyCheckEqual(x, ...rest) {
return Object.is(x, rest[0]) &&
(rest.length < 2 || recursivelyCheckEqual(...rest));
}

增强的对象语法

ES6新增语法糖(为了让编程更简洁优美添加的语法,比如for循环就是基于while的语法糖)

属性值简写

  • 属性名和变量名一样obj{name:name}时可以省略成obj{name}

可计算属性/符号属性

  • 可以使用变量的值作为属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 以前
    const a="aaa"
    let obj1={}
    obj1[a]="bbb"
    // {aaa: 'bbb'}

    // 可计算属性
    let obj2={
    [a]:"ccc"
    }
    // {aaa: 'ccc'}

方法名简写

  • 以前定义对象中的方法:fun : function(val){...}
  • 现在简写:fun(val){...}

对象解构

p362

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用对象解构
let person = {
name: 'Matt',
age: 27
};
let { name: personName, age: personAge } = person;
console.log(personName); // Matt
console.log(personAge); // 27

// 简写,变量名和属性名一致
let { name, age, job} = person;
console.log(name); // Matt
console.log(age); // 27
console.log(job); // undefined

嵌套结构p364

创建对象

创建对象的方式 *

ES6正式支持类和继承

创建对象方式:

  • 对象字面量
  • 工厂模式
  • 构造函数模式
  • 原型模式
1
2
3
4
5
6
person={
name:"Mark",
sayName(){
console.log(this.name)
}
};

工厂模式

一个工厂能提供一个创建对象的公共接口,我们可以在其中指定我们希望被创建的工厂对象的类型,也就是工厂函数就是这个接口,调用函数,返回一个实例,不需要new

1
2
3
4
5
6
7
8
9
10
11
12
function createPerson(name, age, job) {
let o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name);
};
return o;
}
let p1=createPerson("a",20,"student")
let p2=createPerson("b",20,"teacher")

优点:可以创建多个类似对象

缺点:没有解决对象标识问题(不知道新创建的对象是什么类型)

构造函数模式

1
2
3
4
5
6
7
8
9
10
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
let p1=new Person("a",20,"student")
let p2=new Person("b",20,"teacher")

与工厂模式的区别:

  • 没有显式的new Object;属性方法赋给了this,没有return,创建对象时要new
  • 另,构造函数函数名要首字母大写比较规范
  • 另另,构造函数写成函数声明或是函数表达式都可以

优点:可以识别创建对象的类型(访问实例的Constructor属性)

缺点:构造函数定义的方法会在每个实例都创建一遍

  • 一种解决方法是把函数定义在对象外,对象里引用,但是这样就污染了全局作用域,不好,解决方法见原型模式

原型模式 *

每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。使用原型对象的好处是,在它上面 定义的属性和方法可以被对象实例共享

可以用函数声明或函数表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person() {
instance=[]
}
Person.prototype.name=[]
Person.prototype.age=20
Person.prototype.job="student"
Person.prototype.sayName=function(){
console.log(this.name)
}
let p1=new Person()
let p2=new Person()
console.log(p1.sayName===p2.sayName) // true
p1.name.push(1)

注意:

  • 通过prototype定义的属性存在在原型上,被所有实例共享
  • 在构造函数内定义的属性(例中instance),只有在创建实例之后,才会为每个实例单独分配一个
  • 上面两者的区别在属性值为对象时很明显:第一种只要在一个实例里修改了这个对象,原型里和其它实例里都会变,第二种就是独立的

优点:解决了实例属性和方法共享的问题

缺点:需要在外部依次定义prototype的属性,比较繁琐(不能一次性定义,因为prototype会被设置成一个通过字面量创建的新对象,它的constructor就不指向Person了,见p386)

1
2
3
4
5
6
7
8
9
10
11
// 用字面量批量定义属性会有问题!!!不要用!!!
function Person() {}
Person.prototype={
name: "a",
age: 20,
job:"student",
sayName() {
console.log(this.name)
}
}
Person.rototype.constructor

01 理解原型 *

构造函数、原型对象和实例

  • 实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。

1)构造函数创建之后,自动创建一个原型对象属性prototype,指向原型对象

  • 原型对象自动获得一个constructor属性,指向与之关联的构造函数
  • 在自定义构造函数时,原型对象默认只会获得constructor属性, 其他的所有方法都继承自Object。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person() {}

console.log(typeof Person.prototype); // object
console.log(Person.prototype);
// {
// constructor: f Person(),
// __proto__: Object
// }

console.log(Person.prototype.constructor === Person); // true

console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Person.prototype.__proto__.constructor === Object); // true
console.log(Person.prototype.__proto__.__proto__ === null); // true
console.log(Person.prototype.__proto__);

2)每次调用构造函数创建一个新实例(person1)

  • 这个实例的内部**[[Prototype]]指针就会被赋值为构造函数的 原型对象**(Person.prototype)。

  • 脚本中没有访问这个**[[Prototype]]特性的标准方式, 但Firefox、Safari和Chrome会在每个对象上暴露__proto__属性,通 过这个属性可以访问对象的原型**。在其他实现中,这个特性完全被 隐藏了。

1
2
3
4
5
6
7
8
9
10
11
let person1 = new Person(),
person2 = new Person();

console.log(person1 !== Person); // true
console.log(person1 !== Person.prototype); // true
console.log(Person.prototype !== Person); // true

console.log(person1.__proto__ === Person.prototype); // true
conosle.log(person1.__proto__.constructor === Person); // true

console.log(person1.__proto__ === person2.__proto__); // true

检查、修改构造函数、原型对象和实例的关系

  • A instanceof B 运算符

    • 检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
  • isPrototypeOf() 方法

    • 检测原型对象和实例的关系
  • getPrototypeOf() 方法

    • 返回实例对象对应的[[Prototype]]值
  • setPrototypeOf() 方法

    • 修改实例对象对应的[[Prototype]]值,不推荐使用
1
2
3
4
5
6
7
8
console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true
console.log(Person.prototype instanceof Object); // true

console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Person.prototype.isPrototypeOf(person2)); // true

console.log(Object.getPrototypeOf(person1) == Person.prototype); // true

02 原型层级

在通过对象访问属性:按照属性名称开始搜索。

  • 先搜索对象实例本身。如果发现该属性名称,则返回对应值。
  • 如果在实例对象没有找到该属性,则沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值。

属性遮蔽:

  • 在对象实例添加属性,则会遮蔽原型对象的同名属性(不修改原型,只屏蔽访问)
  • 可以通过delete操作符删除实例的属性,回复对原型的访问
1
2
person1.name="b"
delete person1.name

判断一个对象是否包含某个属性 *

  • in 操作符

    • 通过对象可以访问指定的属性时,返回true

      1
      2
      ‘name’ in person1;  // true  
      ‘toString’ in person1; // true
  • hasOwnProperty()

    • 属性存在于实例,返回true,存在于原型或继承的false

      1
      2
      person1.hasOwnProperty(‘name’); //true
      person1.hasOwnProperty(‘toString’); //false

03 原型和in操作符 *

in操作符两种使用方式:

  • 单独:可以通过对象访问指定的属性时,返回true

  • for in循环:返回可以通过对象访问可以被枚举 的属性

    • 实例属性和原型属性都可以
    • 可枚举就表示属性的[[enumberable]]特性为true

获得对象上的所有属性名称(字符串):

  • Object.keys() 返回对象自身所有可枚举属性
  • sObject.getOwnPropertyNames() 返回对象自身所有属性
  • Object.getOwnPropertySymbols() ES6新增,返回对象自身所有符号属性(见Symbol
1
2
3
4
5
6
7
let keys = Object.keys(Person.prototype);
console.log(keys);
// "[name,age,job,sayName]"

let keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys);
// "[constructor,name,age,job,sayName]"

04 枚举顺序

for in 和 Object.keys() 顺序不确定 见p384

对象迭代

ES2017新增 迭代对象的静态方法

  • Object.keys()

  • Object.values() :返回对象属性值的数组

  • Object.entries() :返回对象属性键值对的数组

注:非字符串属性会转换为字符串,符号属性会忽略,值为对象时执行浅拷贝

04 原型的问题 *

p390

原型上的属性在所有实例之间共享

  • 可以 通过在实例上添加同名属性来简单地遮蔽原型上的属性
  • 但是,对于包含引用值的属性,会有问题:
    • 一个实例修改了这个属性的值,其实是修改了原型上的属性值,会反映到其他所有实例上
  • 不同实例之间应该有属于自己的属性副本,所以开发时不会单独使用原型模式

继承

通过原型链实现继承

原型链 *

构造函数、原型 和实例的关系:

  • 构造函数都有一个原型对象,Person.prototype
  • 原型有一个属性指回 构造函数,Person.prototype.constructor===Person
  • 实例有一个内部指针指向原型。person.__proto__===Person.prototype

原型链就是:一个构造函数的原型是是另一个类型的实例,这样整个原型就有一个内部指针([[Prototype]])指向另一个原型,即子类的原型指向父类的原型

ES5的继承写法:将子类的prototype对象重新定义为一个父类的实例(默认所有引用类型都是继承自Object)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 () {
dreturn this.subproperty;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // true

  • A instanceof B 运算符

    • 检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

      1
      2
      3
      console.log(instance instanceof Object); // true
      console.log(instance instanceof SuperType); // true
      console.log(instance instanceof SubType); // true
  • isPrototypeOf() 方法

    • 检测原型对象和实例的关系

      1
      2
      3
      console.log(Object.prototype.isPrototypeOf(instance)); // true
      console.log(SuperType.prototype.isPrototypeOf(instance)); // true
      console.log(SubType.prototype.isPrototypeOf(instance)); // true

03 增加方法

如果子类需要覆盖父类的方法或增加父类没有的方法,那么需要在子类的原型上定义(注意:子类的原型需要先改写成父类的实例再定义方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
};

04 原型链的问题 *

原型中包含的引用值会在所有实例之间共享,所以,在使用原型实现继承时

  • 子类原型是父类的实例。
  • 父类的实例属性变成了子类的原型属性。
    • 实例属性本来是每个实例单独拥有的,但是变成原型属性之后就会变成共享状态
  • 属性值为对象时:由于原型属性在所有子类间共享,所以一个实例改变这个值,所有属性也都改变
1
2
3
4
5
6
7
8
9
10
11
12
13
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {}
// 继承SuperType
SubType.prototype = new SuperType();
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);
// "red,blue,green,black"
let instance2 = new SubType();
console.log(instance2.colors);
// "red,blue,green,black"

盗用构造函数 *

为了解决原型包含引用值导致的继承问题

盗用构造函数/对象伪装/经典继承

  • 在子类的构造函数中调用父类的构造函数

  • 使用call()或apply()进行调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SuperType(name) {
this.colors = ["red", "blue", "green"];
this.name = name
}
function SubType(name) {
// 盗用构造函数继承SuperType
SuperType.call(this, name);
}
let instance1 = new SubType("ins1");
instance1.colors.push("black");
console.log(instance1.colors);
// "red,blue,green,black"
let instance2 = new SubType("ins2");
console.log(instance2.colors);
// "red,blue,green"

优点:解决了引用值的问题,且可以在子类构造函数向父类构造函数传参

问题:在构造函数中定义方法,会在每个实例中创建一个新的方法,因此函数不能重用,在子类也是创建一个新方法,而不是访问父类原型上定义的方法

组合继承 **

组合继承/伪经典继承:

  • 通过原型链继承原型上的属性和方法:重用方法
  • 通过盗用构造函数继承实例属性:每个实例都有单独的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function SuperType(name){
// 实例属性
this.name = name;
this.colors = ["red", "blue", "green"];
}
// 原型方法
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age){
// 继承属性
SuperType.call(this, name);
this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
console.log(this.age);
};
let instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // "Nicholas";
instance1.sayAge(); // 29
let instance2 = new SubType("Greg", 27);
console.log(instance2.colors); // "red,blue,green"
instance2.sayName(); // "Greg";
instance2.sayAge(); // 27

原型式继承

适用情况:不需要单独创建构造函数,但仍然需要在对象间共享信息的场合,也就是在一个对象的基础上创建新对象

使用Object.create() 方法,传入参数:作为原型的对象,新增的属性(通过描述符表示)

1
2
3
4
5
6
7
8
9
10
11
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
// 原型式继承
let anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
});
console.log(anotherPerson.name); // "Greg"

优点:不在意类型和构造函数

缺点:和原型模式一样,引用值会共享

寄生式继承

创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createAnother(original){
let clone = object(original); // 通过调用函数创建一个新对象
clone.sayHi = function() { // 以某种方式增强这个对象
console.log("hi");
};
return clone; // 返回这个对象
}
// 使用createAnother函数
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"

优点:不在意类型和构造函数

缺点:与盗用构造函数模式类似,函数难以重用

寄生式组合继承 *

3.3的组合继承存在问题:

子类的原型上会多出一组实例属性,因为:p403

  • 声明父类构造函数时,定义了实例属性

  • 将子类原型定义成父类实例时,创建了一组实例属性

寄生式组合继承就是解决上面的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function inheritPrototype(subType, superType) {
let prototype = Object.create(superType.prototype); // 取得父类的原型的副本作为子类的原型
prototype.constructor = subType; // 重新设置constructor使其指向子类构造函数
subType.prototype = prototype; // 子类的原型指向副本
}

function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name); // 继承实例属性
this.age = age;
}
// 继承方法属性,这里跟3不一样,3用的是子类原型是父类实例
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
};

优点:是引用类型继承的最佳模式

ES6新加入的语法糖

类定义

建议类名首字母大写

1
2
3
4
// 类声明
class Person {}
// 类表达式
const Animal = class {};

和函数的区别:

  • 函数声明可以提升,类定义不能

  • 函数受函数作用域限制,类受块作用域限制

类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法(方法名前加static只能在类中调用)

类构造函数

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
constructor(name) {
this.name = name || "a";
}
this.name
static sayName (){
console.log(this.name)
}
xhckhck
}
let p = new Person();
p.name

实例化的过程见p408

类构造函数和构造函数的区别:

  • 类构造函数一定要new调用!不然报错
  • 普通构造函数不new的话就会默认变成全局对象

实例 原型 类成员

实例成员

  • 通过类构造函数的this添加,或直接给创建好的实例添加,每个实例都有唯一成员对象,不会共享

原型方法

  • 类块中定义的方法,共享,等于属性,跟对象一样可以用字符串,符号,或计算的值为键,setter,getter也一样

静态类方法

  • 用static关键字作前缀
  • 属于类,this引用类自身,不属于类实例,只能被类调用

非函数类型和类成员

  • 类块里面不能直接定义类成员,但是类块外面可以用类名.成员名定义

继承

01 基础

extends关键字继承任何有[[Construct]]和原型的对象(可以继承类或构造函数,向后兼容)

1
2
3
4
// 类声明
class Bus extends Vehicle {}
// 类表达式也可以
let Bar = class extends Foo {}

02 构造函数 *

通过super调用父类构造函数和静态方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Vehicle {
constructor() {
this.hasEngine = true;
}
static say1 () {
console.log(1)
}
}
class Bus extends Vehicle {
constructor() {
// 先super()调用构造函数,不能在super前this
super(); // 相当于super.constructor()
console.log(this instanceof Vehicle); // true
console.log(this); // Bus { hasEngine: true }
}
// 调用静态方法
static say1(){
super.identify()
}
}
new Bus();

03 抽象基类

可以被其他类继承,但本身不会被实例化。虚基类?

  • new.target保存通过new关键字调用的类 或函数。通过在实例化时检测new.target是不是抽象基类,可以阻止对抽象基类的实例化

  • 通过在抽象基类构造函数中进行检查,可以要求派生类必须 定义某个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 抽象基类
class Vehicle {
constructor() {
if (new.target === Vehicle) {
throw new Error('Vehicle cannot be directly instantiated');
}
if (!this.foo) {
throw new Error('Inheriting class must define foo()');
}
console.log('success!');
}
}
// 派生类
class Bus extends Vehicle {
foo() {}
}
// 派生类
class Van extends Vehicle {}
new Bus(); // success!
new Van(); // Error: Inheriting class must define foo()

05 类混入

多类继承:p425

  • 在一个表达式中连缀多个混入元素,这个 表达式最终会解析为一个可以被继承的类。
  • 例:想要P组合ABC:B基础A,C继承B,然后P继承C
  • 已抛弃,用组合模式(把方法提取到独立的类和辅助对象中, 然后把它们组合起来,但不使用继承)

总结

理解原型

构造函数、原型对象和实例

  • 实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。

1)构造函数创建之后,自动创建一个原型对象属性prototype,指向原型对象

  • 原型对象自动获得一个constructor属性,指向与之关联的构造函数
  • 在自定义构造函数时,原型对象默认只会获得constructor属性, 其他的所有方法都继承自Object。

2)每次调用构造函数创建一个新实例

  • 这个实例的内部**[[Prototype]]指针就会被赋值为构造函数的 原型对象**
  • 脚本中没有访问这个**[[Prototype]]特性的标准方式, 但Firefox、Safari和Chrome会在每个对象上暴露__proto__属性,通 过这个属性可以访问对象的原型**

在通过对象访问属性:按照属性名称开始搜索。

  • 先搜索对象实例,发现属性则返回
  • 未发现,则沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值。

原型的问题:原型上的属性在所有实例之间共享

  • 可以 通过在实例上添加同名属性来简单地遮蔽原型上的属性
  • 但是,对于包含引用值的属性,会有问题:
    • 一个实例修改了这个属性的值,其实是修改了原型上的属性值,会反映到其他所有实例上
  • 不同实例之间应该有属于自己的属性副本,所以开发时不会单独使用原型模式

理解原型链

构造函数、原型 和实例的关系:

  • 构造函数都有一个原型对象,Person.prototype
  • 原型有一个属性指回 构造函数,Person.prototype.constructor===Person
  • 实例有一个内部指针指向原型。person.__proto__===Person.prototype

原型链就是:一个构造函数的原型是是另一个类型的实例,这样整个原型就有一个内部指针([[Prototype]])指向另一个原型,即子类的原型指向父类的原型

原型链的问题:原型中包含的引用值会在所有实例之间共享,所以,在使用原型实现继承时

  • 子类原型是父类的实例。
  • 父类的实例属性变成了子类的原型属性。
    • 实例属性本来是每个实例单独拥有的,但是变成原型属性之后就会变成共享状态
  • 属性值为对象时:由于原型属性在所有子类间共享,所以一个实例改变这个值,所有属性也都改变

继承的方法

  1. 原型链继承:包含引用值的属性会共享
  2. 盗用构造函数继承:无法重用方法
  3. 原型链+盗用构造函数组合继承:1+2 使用最多
  4. 原型式继承:没有构造函数,缺点类似1
  5. 寄生式继承:没有构造函数,缺点类似2
  6. 寄生式组合继承:2+4,最有效
  7. 继承,好

原型链

优点:共享原型方法

缺点:原型对象是引用值时,共享引用,改一个实例就全改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1. 原型链
function Super(val){
this.val=1 // 实例属性
}
Super.prototype.arr=[] // 原型属性
Super.prototype.getArr=function(){ // 原型方法
console.log(this.arr)
}

function Sub(val,str){
this.str=str
}
Sub.prototype=new Super(this.val) // 继承原型
Sub.prototype.getStr=function(){
console.log(this.str)
}

let s=new Sub(1,"1")
let t=new Sub(2,"2")
console.log(s.arr===t.arr) // true 共享原型对象
console.log(s.getArr===t.getArr) // true 共享原型方法

盗用构造函数

优点:实例有各自的属性值

缺点:方法只能定义在构造函数里,难以重用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 2. 盗用构造函数
function Super(val){
this.val=val // 实例属性
this.arr=[]
this.getArr=function(){ // 实例方法
console.log(this.arr)
}
}

function Sub(val, str){
// 盗用构造函数继承原型,可以传值了
Super.call(this, val)
this.str=str
this.getStr=function(){
console.log(this.str)
}
}

let s=new Sub(1)
let t=new Sub(2)
console.log(s.arr===t.arr) // false 各自的实例对象
console.log(s.getArr===t.getArr) // false 不共享方法

组合继承

优点:实例有各自的属性值,不会干扰,方法也可以重用

缺点:子类的原型上会多出一组实例属性,因为:p403

  • 声明父类构造函数时,定义了实例属性

  • 将子类原型定义成父类实例时,创建了一组实例属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 3. 组合继承
function Super(val){
this.val=val // 实例属性
this.arr=[]
}
Super.prototype.getArr=function(){ // 对象方法
console.log(this.arr)
}

function Sub(val,str){
// 盗用构造函数继承原型,可以传值了
Super.call(this, val)
this.str=str
}
Sub.prototype=new Super()
Sub.prototype.getStr=function(){
console.log(this.str)
}

let s=new Sub(1,"1")
let t=new Sub(2,"2")
console.log(s.arr===t.arr) // false 各自的实例对象
console.log(s.getArr===t.getArr) // true 共享原型方法

寄生式组合

优点:解决了普通组合式的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 6. 寄生式组合继承
function inheritPrototy(subType,superType){
// 浅复制父类的原型
let prototype=Object.create(subType.prototype)
// 修正constructor
prototype.constructor=subType
// 让子类的原型指向父类的原型
subType.prototype=prototype
}

function Super(val){
this.val=val // 实例属性
this.arr=[]
}
Super.prototype.getArr=function(){ // 对象方法
console.log(this.arr)
}

function Sub(val,str){
// 盗用构造函数继承原型,可以传值了
Super.call(this, val)
this.str=str
}
inheritPrototy(Sub,Super)
Sub.prototype.getStr=function(){
console.log(this.str)
}

let s=new Sub(1,"1")
let t=new Sub(2,"2")
console.log(s.arr===t.arr) // false 不共享实例属性
console.log(s.getArr===t.getArr) // true 共享原型方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Super{
constructor(val){
this.val=val
this.arr=[]
}
getArr(){
console.log(this.arr)
}
}

class Sub extends Super{
constructor(val,str) {
super(val)
this.str=str
}
getStr(){
console.log(this.str)
}
}

let s=new Sub(1,"1")
let t=new Sub(2,"2")
console.log(s.arr===t.arr) // false 不共享实例属性
console.log(s.getArr===t.getArr) // true 共享原型方法

区别:prototype__proto__

构造函数方法

每一个构造函数有一个prototype指针,指向构造函数声明时自动创建的原型对象

只要是对象就会有一个[[Prototype]]内部属性,这个属性在chrome浏览器中可以被__proto__属性暴露出来

  • new出来的实例对象的__proto__指向构造函数原型对象
  • 构造函数的__proto__指向Function原型对象,因为本质上它是一个函数
  • 任何原型对象的__proto__指向Object原型对象,因为本质上它是一个对象
1
2
3
4
5
function Person(){}
let p=new Person()
Person.prototype===p.__proto__ // true
Person.__proto__===Function.prototype // true
Person.prototype.__proto__===Object.prototype // true

构造函数继承

原型链继承时,本质上是把子类的原型指向父类的实例,因为父类的实例的__proto__指针能指向父类原型

  • 子类的原型上的__proto__指向父类原型

  • 子类实例上的__proto__指向子类原型,本质上是父类的实例

1
2
3
4
5
function Adult(){}
Adult.prototype=new Person()
let a=new Adult()
Adult.prototype.__proto__===Person.prototype // true
Adult.prototype===a.__proto__ // true

类继承

使用class声明类时prototype__proto__的指向跟构造函数时完全一样

  • new出来的实例对象的__proto__指向构造函数原型对象
  • 构造函数的__proto__指向Function原型对象,因为本质上它是一个函数
  • 类的原型对象的__proto__指向Object原型对象,因为本质上它是一个对象
1
2
3
4
5
class Person{}
let p=new Person()
Person.prototype===p.__proto__ // true
Person.__proto__===Function.prototype // true
Person.prototype.__proto__===Object.prototype // true
  • 子类的原型上的__proto__指向父类原型
  • 子类实例上的__proto__指向子类原型
1
2
3
4
class Adult extends Person {}
let a=new Adult()
Adult.prototype.__proto__===Person.prototype // true
Adult.prototype===a.__proto__ // true

注意,*类的成员方法就是定义在类的原型上的

------ 本文结束 ❤ 感谢你的阅读 ------
------ 版权信息 ------

本文标题:红宝书学习:第八章 对象,类,面向对象

文章作者:Lury

发布时间:2022年02月24日 - 17:10

最后更新:2022年04月14日 - 23:34

原始链接:https://luryzhu.github.io/2022/02/24/JavaScript/prof_ch8/

许可协议:署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。