JS知识点梳理(4)-面向对象

理解对象

ECMA 把对象定义为“无序属性的集合,其属性可以包含基本值、对象或函数”。也可把对象想象成散列表:无非就是一组名值对,其值可以是基本值、对象或函数。
每个对象都是基于一个引用类型创建的,这个引用类型可以是原生类型,也可以是开发人员自己定义的类型。

属性类型

对象的属性在创建时都带有一些特征值,JS 通过这些特征值来定义它们的行为。这些特性时内布值,不能直接访问它们,将其放在 2 对方括号内。其分为 2 中属性:数据属性和访问器属性

数据属性

数据属性有 4 个描述其行为的特性:

  • [[Configurable]]: 表示 能否通过delete删除属性,能否修改属性的特性,能否把属性修改为访问器属性。默认值为true
  • [[Enumerable]]: 表示 是否可枚举,通过for..in循环返回属性。 默认值为true
  • [[Writable]]: 表示 能否修改属性,默认值为true
  • [[Value]]: 此属性的数据值。读取属性值时,从这里读;写入属性值时,把新值保存在这里。默认值为undefined

要修改属性默认的特性,必须使用Object.defineProperty(obj,prop,descriptor).

在调用Object.defineProperty()方法创建一个新属性时,描述符对象如果不指定特性值,configurable,enumerable,writable特性的默认值时false.如果是修改已定义的属性的特性,则无此限制

1
2
3
4
5
6
7
8
9
10
11
12
13
var person = {};
Object.defineProperty(person, "name", {
value: "jiang",
});
console.log(person.name); // "jiang"
person.name = "amile";
// 创建新 name 属性时, writable,configurable,enumerable 默认值为false . 所有不可重写,不可配置
console.log(person.name); // "jiang"
console.log(Object.getOwnPropertyDescriptor(person, "name"));
Object.defineProperty(person, "name", {
configurable: true,
value: "amile"
}); //

访问器属性

访问器属性不包含数据值。但包含一对 getter,setter 函数(都不是必需的)。在读取访问器属性时,会调用 getter 函数,返回有效的值;在写入访问器属性时,会调用 setter 函数并传入新值,此函数决定如何处理函数。

  • [[Configurable]]: 同数据属性中的[[Configurable]]
  • [[Enumerable]]: 同数据属性中的[[Enumerable]]
  • [[Get]]: 在读取属性时调用的函数。默认值为undefined
  • [[Set]]: 在写入属性时调用的函数。默认值为undefined

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

读取属性的特性

ECMAScript 5 的Object.getOwnPropertyDescriptor(obj,prop),可取得给定属性的描述符对象。

  • 如果是数据属性,该对象的属性有configurable,enumerable,writable,value;
  • 如果是访问器属性,该对象的属性有configurable,enumerable,get,set
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var book = {};
Object.defineProperties(book, {
_year: {
value: 2020
},
edition: {
value: 1
},
year: {
get: function() {
return this._year;
},
set: function(newValue) {
if (newValue > 2020) {
this._year = newValue;
this.edition = newValue - 2020 + 1;
}
}
}
});

console.log(Object.getOwnPropertyDescriptor(book, "_year")); // Object {value: 2020, writable: false, enumerable: false, configurable: false}
console.log(Object.getOwnPropertyDescriptor(book, "year")); // Object {get: function() {...} , set: function(newValue) {...}, enumerable: false, configurable: false}

创建对象

工厂模式

用函数来封装创建对象的细节.有致命缺点,不知道对象的类型,没有解决对象识别问题

1
2
3
4
5
6
7
8
9
10
11
// 工厂模式
function createPerson(name, age) {
var person = new Object();
person.name = name;
person.age = age;
person.sayName = function() {
console.log(this.name);
};
return person;
}
var person1 = createPerson("jiang", 26);

构造函数模式

创建自定义的构造函数。按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数以一个小写字母开头。

创建实例的过程

要创建新实例,必须使用new操作符,以此方式调用构造函数实际会经历以下步骤:

    1. 创建一个新对象
    1. 将构造函数的作用域赋给新对象,即 this也就指向了这个新对象
    1. 执行构造函数中的代码,为新对象添加属性
    1. 返回该新对象
1
2
3
4
5
6
7
8
9
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function() {
console.log(this.name);
}
}
var person1 = new Person("jiang", 26);
var person2 = new Person("amile", 22);

构造函数模式的优缺点

优点

对象的constructor属性指向创建它的构造函数,因此可以使用instanceof操作符来 检测对象类型。

1
2
3
4
var person1 = new Person("jiang", 26);
console.log(person1.constructor === Person); // true
console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true
缺点

构造函数内定义的每个方法,都要在每个实例上重新创建一遍。该问题可以通过原型模式来解决

原型模式

  • 每一个函数都一个prototype(原型)属性,它是一个指针,指向一个对象。该对象包含 使用该构造函数创建的所有实例 共享的属性和方法
  • prototype就是通过调用构造函数而创建的那个对象实例的 原型对象
  • 原型对象包含的属性和方法,共享给所有对象实例。
    因此,不必在构造函数定义对象实例要共享的信息,而是把这些信息添加到原型对象中。
1
2
3
4
5
6
7
8
9
10
11
function Person() {}
Person.prototype.name = "jaing";
Person.prototype.age = 26;
Person.prototype.sayName = function() {
console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.sayName(); // "jiang"
console.log(person2.age); // 26
console.log(person1.sayName === person2.sayName); // true

理解原型对象

  • 只要创建了一个新函数,就会创建一个prototype属性,这个属性指向函数的原型对象
  • 所有的原型对象都会自动包含一个constructor属性,它是一个指向prototype属性所在函数的指针。因为该属性在原型对象中,所以也是共享的,可通过对象实例访问。person.constructor === Person,Person.prototype.constructor === Person
  • 但调用构造函数创建一个新实例后,该实例内部将包含一个指针([[Prototype]],内部属性),指向构造函数的原型对象。在浏览器中,每个对象都支持一个属性__proto__来访问[[Prototype]].
  • 实例的[[Prototype]] 或 __proto__指向的是构造函数的原型对象。该连接存在于实例和构造函数的原型对象之间,而不存在于实例和构造函数之间
  • ECMAScript 5 新增Object.getPrototypeOf() 可返回对象的原型,即一个对象的[[Prototype]] 或 __proto__的值。即Object.getPrototypeOf(person1) === Person.prototypeObject.getPrototypeOf(person1) === person1.__proto__
  • hasOwnProperty()方法 ,可以检测一个属性是否存在于实例中,还是存在于原型中。只有给定属性存在于实例对象中,才返回 true

原型 于 in 操作符

  • 单独使用 in 操作符:只要对象能够访问指定属性就返回 true,无论该属性存在于实例还是原型中。

    1
    2
    3
    4
    5
    6
    console.log(person1.hasOwnProperty("name")); // false  name 来自 原型
    console.log("name" in person1); // true
    person1.name = "qiang";
    console.log(person1.hasOwnProperty("name")); // true name 来自 实例
    console.log("name" in person1); // true

  • for...in循环:循环所以能通过对象访问的,可枚举的属性,包含存在于实例中的,也包括存在于原型中的属性。会屏蔽 不可枚举的属性(即[[Enumerable]]标记为 false)。constructor,prototype是不可枚举的。

  • Object.keys(),获取对象上所有可枚举的实例属性。

  • Object.getOwnPropertyNames(),获取对象上所有实例属性,无论它是否可枚举。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person() {}
Person.prototype.name = "jaing";
Person.prototype.age = 26;
Person.prototype.sayName = function() {
console.log(this.name);
};
var person1 = new Person();
person1.school = "26 school";
var props = [];
for (var v in person1) {
props.push(v);
}
console.log(props); // ["name", "school", "age", "sayName"]

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

更简单的原型语法

前面例子中每添加一个属性或方法就要敲一遍Person.prototype,为减少输入,通常会使用一个包含所有属性和方法的对象字面量来重写整个原型对象。
在将Person.prototype设置为一个新对象,其constructor属性对不再指向Person 构造函数了,而是指向了新对象的constructor属性,即 Object 构造函数。此时,尽管instanceof操作符返回正确结果,但通过constructor已经无法确定对象类型了。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person() {}
Person.prototype = {
name: "jiang",
age: 26,
sayName: function() {
console.log(this.name);
}
};

var friend = new Person();
console.log(friend instanceof Person); // true
console.log(friend.constructor === Person); // false
console.log(friend.constructor === Object); // true

解决此问题,可以手动设置一个constructor属性,值为 Person,且不可枚举。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person() {}
Person.prototype = {
name: "jiang",
age: 26,
sayName: function() {
console.log(this.name);
}
};

Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
var friend = new Person();
console.log(friend instanceof Person); // true
console.log(friend.constructor === Person); // true
console.log(friend.constructor === Object); // false

原型的动态性

由于在原型中查找值的过程是一次搜素,因此对原型对象所做的任何修改都能立即从实例反映出来,即使是先创建了实例之后,再修改原型也照样如此
实例于原型之间的连接([[Prototype]] 或者 __proto__)是一个指针,而非副本.如果重新整个原型,就会切断现有原型同之前已经存在的对象实例之间的联系。之前对象实例引用的仍然是最初的原型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person() {}
var friend = new Person();
Person.prototype.sayName = function() {
console.log("hi");
};
friend.sayName(); // hi ,在原型中查找
Person.prototype = {
constructor: Person,
name: "jiang",
age: 26,
sayName: function() {
console.log(this.name);
}
};
console.log(friend.name); // jiang
friend.sayName(); // hi ,在最初的原型中查找

原型对象的问题

  • 它省略了为构造函数传参,结果所有实例在默认情况下都是相同的属性值。

  • 最大问题,对应包含引用类型值的属性,在一个实例上修改,会被多个实例共享

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function Person() {}
    Person.prototype = {
    constructor: Person,
    name: "jiang",
    age: 26,
    sayName: function() {
    console.log(this.name);
    },
    friends: ["a", "b"]
    };

    var person1 = new Person();
    var person2 = new Person();
    person1.friends.push("c");
    console.log(person1.friends); // ["a", "b", "c"]
    console.log(person2.friends); // ["a", "b", "c"]
    console.log(person1.friends === person2.friends); // true

组合使用构造函数模式和原型模式 (最理想的模式)

创建自定义类型最常见的方式,就是组合使用构造函数模式和原型模式。

  • 构造函数模式用于定义实例属性
  • 原型模式有于定义方法和共享的属性

每个实例都有自己的一份实例属性副本,同时又共享着对方法的引用,最大限度的节省了内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 组合模式
function Person(name, age) {
this.name = name;
this.age = age;
this.friends = ["a", "b"];
}
Person.prototype = {
constructor: Person,
sayName: function() {
console.log(this.name);
}
};
var person1 = new Person("jiang", 26);
var person2 = new Person("ting", 25);
person1.friends.push("c");
console.log(person1.friends); // ["a", "b", "c"]
console.log(person2.friends); // ["a", "b"]
console.log(person1.sayName === person2.sayName); // true

动态原型模式

使用组合模式,构造函数和原型是独立的,动态原型模式则把所有信息都封装在了构造函数中,并在必要情况下,在构造函数内初始化原型。也同时保存了使用构造函数和原型的优点。

1
2
3
4
5
6
7
8
9
10
11
//  动态原型模式
function Person(name, age) {
this.name = name;
this.age = age;
this.friends = ["a", "b"];
}
if (typeof this.sayName !== "function") {
Person.prototype.sayName = function() {
console.log(this.name);
};
}

寄生构造函数模式

基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后返回新创建的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name, age) {
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function() {
console.log(this.name);
};
return o;
}
var person4 = new Person("tingyu", 28);
person4.sayName();
console.log(person4 instanceof Person); // false
console.log(person4.constructor); // function Object {...}
console.log(Object.getPrototypeOf(person4)); // Object {...}

寄生构造函数模式,返回的对象于构造函数或者构造函数的原型属性之间没有关系,所有,不能依赖 instanceof 来确定对象类型

对象继承

由于函数没有签名,在 ECMAScript 中无法实现接口继承,所以只支持实现继承,并主要依靠原型链来实现的。基本思路就是利用原型让一个引用类型继承另一个引用类型的属性和方法。

原型链

构造函数、原型、实例间关系

  • 每个构造函数都有一个原型对象(Fn.prototype
  • 原型对象都包含一个指向构造函数的指针(Fn.prototype.constructor)
  • 实例都包含一个指向原型对象的内部指针(instance.__proto__)

让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型对象的指针,相应的,另一个原型中也包含着指向另一个构造函数的指针。假如,另一个原型又是另一个类型的实例,依次层层递进,就构成了实例与原型的链条

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() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};

function SubType() {
this.subproperty = false;
}
// 继承
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function() {
return this.subproperty;
};

var instance = new SubType();
console.log(instance.getSubValue()); // false
console.log(instance.getSuperValue()); // true
console.log(instance instanceof SubType); // true
console.log(instance instanceof SuperType); // true
console.log(Object.getPrototypeOf(instance)); // SuperType {...}
console.log(instance.constructor); // function SuperType() {...}
// instanceof 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链
// Object.getPrototypeOf() 方法返回指定对象的原型(内部[[Prototype]]属性的值)

在上面代码中,没有使用 SubType 默认提供的原型,而是给它换了一个新原型,就是 SuperType 的实例。
新原型不仅具有 SuperType 的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向 SuperType 的原型。
所以,instance 指向 SubType 的原型,SubType 的原型又指向 SuperType 的原型。
另外,instance.constructor现在指向的是SuperType,这是因为原来SubType.prototype中的constructor被重写的缘故。

原型搜索机制

在通过原型链实现继承的情况下,搜索过程就是沿着原型链继续向上,直到原型链末端null停止。
当访问一个实例属性时,首先在实例中搜索该属性。如果没有,则会继续搜索实例的原型,如果还没有,则去实例原型的原型中搜索,沿着原型链向上一直搜索,直到原型为 null 为止。

确定原型和实例的关系

  • instanceof:检测构造函数的 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

添加方法注意事项

  • 给原型定义方法,一定要放在替换原型的语句之后
  • 在通过原型链实现继承时,不能用对象字面量创建原型方法,因为这样就会重新原型链

原型链的问题

  • 在通过原型来实现继承时,原型实际上会变成另一个类型的实例,于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function SuperType() {
    this.colors = ["red", "blue", "green"];
    }
    function SubType() {}
    SubType.prototype = new SuperType();
    var instance = new SubType();
    instance.colors.push("white");
    console.log(instance.colors); // ["red", "blue", "green", "white"]
    var instance2 = new SubType();
    console.log(instance2.colors); // ["red", "blue", "green", "white"]
    console.log(instance2.colors === instance.colors); // true
  • 在创建子类型的实例时,不能向超类型的构造函数中传递参数。

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

基本思想就是在子类型构造函数的内部调用超类型构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
function SubType(name,age) {
// 调用SuperType构造函数,继承了 SupperType
SuperType.call(this,name);
this.age = age;
}
var instance = new SubType("jiang",26);
instance.colors.push("white");
console.log(instance.colors); // ["red", "blue", "green", "white"]
var instance2 = new SubType("yuting",28);
console.log(instance2.colors); // ["red", "blue", "green"]
console.log(instance2.colors === instance.colors); // false

借用构造函数的问题

  • 方法都在构造函数中定义,因此函数无法复用
  • 在超类原型上定义的方法,对子类而已是不可见的。

组合继承(最常用的继承模式)

将原型链和借用构造函数的技术组合到一起,从而发挥二者之长。

  • 使用原型链实现对原型属性和方法的继承
  • 通过借用构造函数来实现对实例属性的继承
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
// 组合继承
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.constructor = SubType;
SubType.prototype.sayAge = function() {
console.log(this.age);
};

var instance = new SubType("jiang", 26);
instance.colors.push("white");
console.log(instance.colors); // ["red", "blue", "green", "white"]
instance.sayName(); // "jiang"
instance.sayAge(); // 26
var instance2 = new SubType("yuting", 28);
console.log(instance2.colors); // ["red", "blue", "green"]

instance2.sayName(); // "yuting"
instance2.sayAge(); // 28

原型式继承

借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型的构造函数。为达到这个目的,使用如下函数。先创建一个临时的构造函数,然后将传入的对象作为该构造函数的原型,最后返回这个临时类型的新实例。

1
2
3
4
5
function Object(o) {
function F() {}
F.prototype = o;
return new F();
}

ECMAScript 5 新增Object.create()方法规范了原型式继承,这个方法接受 2 个参数,第一个,为作为新对象原型的对象,第二个,为新对象定义额外属性的对象。其行为同上面Object()方法相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var person = {
name: "jiang",
friends: ["a", "b", "c"]
};
var person2 = Object.create(person);
person2.friends.push("d");
var person3 = Object.create(person, {
name: {
value: "yuting"
}
});

console.log(person3.name); // "yuting"
console.log(person3.friends); // ["a", "b", "c", "d"]
console.log(person.friends); // ["a", "b", "c", "d"]
  • 问题,包含引用类型值的属性始终都会共享相应 的值,就 像使用原型模式一样。

寄生式继承

寄生式继承 的思路同寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回该对象。

1
2
3
4
5
6
7
8
function createAnther(original) {
var clone = Object.create(original);
original.sayHi = function() {
console.log("Hi Hi");
};
return clone;
}
console.log(createAnther({}).sayHi()); // "Hi Hi"

寄生组合式继承 (最理想的继承模式)

组合继承是 JavaScript 中最常用的继承模式,但它有不足之处。会调用 2 次超类型构造函数:一次在创建子类型的原型时。另一次在子类型构造函数内部调用超类构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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); // 第二次调用 SuperType()
this.age = age;
}
// 继承方法
SubType.prototype = new SuperType(); // 第一次调用 SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
console.log(this.age);
};
  • 第一次调用 SuperType 构造函数时,SubType.prototype会得到 2 个属性:namecolors,它们都是 SuperType 的实例属性,只不过位于 SubType 的原型上。
  • 当调用 SubType 构造函数时,会第二次调用 SuperType 构造函数,这次是在新对象 SubType 的实例上创建了实例属性name,colors,于是这 2 个属性屏蔽了原型中的 2 个同名属性。

解决方法– 寄生组合式继承。通过构造函数来继承属性,通过原型链的混成形式来继承方法。不必为了指定子类型的原型而调用超类型的构造 函数,我们需要的无非就是超类型原型的一个副本而已。 简单形式,如下:

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 inheritPrototype(subTyppe, supperType) {
var prototype = Object.create(supperType.prototype); // 获取超类原型副本
prototype.constructor = subTyppe; // 为副本添加constructor 属性
subTyppe.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); // 第二次调用 SuperType()
this.age = age;
}
// 继承方法
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
};
var instance = new SubType("jiang jiang", 26);
instance.sayName();
instance.sayAge();