基础知识

原型

创建的每一个函数都有一个prototype(原型)属性,这个属性是一个指针,指向了一个对象。所有由此函数用new操作符创建出的对象都共享此原型(属性、方法)。

原型对象

任何一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。默认情况下,所有的原型对象都会自动获得一个constructor属性,这个属性指向的是prototype属性所在函数的指针。

1
2
3
4
5
function Person(name) {
this.name = name;
}

console.log(Person.prototype.constructor)

例如上述代码中,Person.prototype.constructor指向的是Person。

输出应该为

1
2
3
function Person(name) {
this.name = name;
}

对于对象来说,浏览器(FireBox、Chrome)都会默认添加一个属性__proto__ ,这个指向的是什么?

1
2
3
4
5
6
7
function Person(name) {
this.name = name;
}

let p = new Person('Bob');

console.log(p.__proto__.constructor)

console输出为

1
2
3
function Person(name) {
this.name = name;
}

所以说,这个__proto__指向的是Person.prototype

判断原型

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name) {
this.name = name;
}

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

let p1 = new Person('Bob');
let p2 = new Person('Tom');

Person.prototype.isPrototypeOf(p1); // console: true
Person.prototype.isPrototypeOf(p2); // console: false

获取原型

1
2
3
4
5
6
7
function Person(name) {
this.name = name;
}

let p = new Person('Bob');

Object.getPrototypeOf(p) == Person.prototype; // console: true

原型和属性的优先级

1
2
3
4
5
6
7
8
9
10
function Person() {
}

Person.prototype.name = "Tom";
let p1 = new Person();
let p2 = new Person();
p1.name = 'Bob'

p1.name; // console: Bob
p2.name; // console: Tom

当对象属性存在和原型属性同时存在时,顺序应当为 对象属性 -> 原型属性进行读取。

判断属性是来自对象还是原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(){
}
Person.prototype.name = 'Cos';

let p1 = new Person();
let p2 = new Person();

p1.hasOwnProperty("name"); // console: false 不存在于自身

p1.name = 'Bob';
p1.name; // console: Bob
p1.hasOwnProperty("name") // console: true 存在于自身的

delete p1.name;
p1.hasOwnProperty("name") // console: false 存在于原型

in操作符

1
2
3
4
5
6
7
8
9
function Person() {  
}

Person.prototype.name = 'Bob';

let p1 = new Person();

console.log("name" in p1); // console: true

使用in操作符可以判断一个属性是否存在于对象或者原型中。

扩展:如果想要判断一个属性是否存在并且是否为对象所属的,我们应该用什么进行判断?

1
2
3
4
5
6
7
8
function Person() {
}

Person.prototype.name = 'Bob';

let p1 = new Person();

!p1.hasOwnProperty("name") && ("name" in p1); // console: true

继承和原型链

继承

在OO(面向对象)语言中,一般都支持两种继承方式:接口继承和实现继承,而在JS中,仅仅支持实现继承这种方式。

原型链

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

let instance = new SubType();
instance.getSuperValue();

由上面的代码可以看出instance指向了SubType的原型,SubType的原型又指向了SuperType的原型。有一个特殊说明:如果调用instance.prototype.constructor,则返回的是SuperType,这是因为被重写了。

其他说明

  1. 属性和方法先会检索对象本身是否存在,如果不存在则沿着原型链向上寻找。
  2. 所有默认的原型都是Object的实例
  3. 确定原型和实例的关系可以用instanceof操作符some instanceof Object
  4. 原型是引用类型,共享原型中的属性,用一个例子说明
1
2
3
4
5
6
7
8
9
10
11
12
13
function SuperType() {
this.colors = ['red', 'blue', 'yello'];
}

function SubType() {
}

SubType.prototype = new SuperType();
let ins1 = new SubType();
ins1.colors.push('white'); // ['red', 'blue', 'yello', 'white']

let ins2 = new SubType();
ins1.colors; // ['red', 'blue', 'yello', 'white']

常用的构造模式

组合使用构造模式和原型模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Tom", "Court"];
}

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

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

person1.friends.push("Van");
console.log(person1.friends);
console.log(person2.friends);
console.log(person1.friends === person2.friends);
console.log(person1.sayName === person2.sayName);

运行结果

1
2
3
4
[ 'Tom', 'Court', 'Van' ]
[ 'Tom', 'Court' ]
false
true

特点总结

  1. 原型可以复用代码,person1.sayName === person2.sayName可以看出指向的同一个内存地址。
  2. 使用构造函数构造的对象,即使有有相同的方法(不放在原型中)也不指向同一内存空间。

动态原型模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;

if (typeof this.sayName !== 'function') {
Person.prototype.sayName = function() {
console.log(this.name);
}
}
}

let person1 = new Person('Bob', 24, 'Teacher');
person1.sayName();

运行结果

1
Bob

特点总结

  1. 当使用动态原型模式的时候,不能够以对象字面量的形式进行重写,否则则会切断所有以创建对象和原型之间的联系,例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
}
// Location A
Person.prototype.test = function() {
console.log('I\'m test');
}

// Location B
let p1 = new Person('Bom', 20, 'Teacher');

// Location C
Person.prototype = {
sayName: function() {
console.log(this.name);
}
}

从上述代码中,如果我们在Location C的地方用字面量的形式重写prototype,则相当于让prototype指向了一块新的内存地址,而在LocationB已经创建好的p1的__proto__,则会于新的原型切断联系。

  1. 通过修改原型的方式,可以让已经创建好对象的__proto__拥有原型中的方法。例如:
1
2
3
4
5
6
7
8
9
10
11
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
}

let p1 = new Person('bob', 21, 'Doctor');

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

则p1也具有了sayName这个属性,不过是使用prototype中的。

寄生构造函数模式

1
2
3
4
5
6
7
8
9
10
11
12
13
function SpecialArray() {
let values = new Array();
values.push.apply(values, arguments);

values.toPipedString = function() {
return this.join('|');
};

return values;
}

let colors = new SpecialArray('red', 'blue', 'green');
console.log(colors.toPipedString());

运行结果

1
red|blue|green

特点总结

  1. 使用寄生构造模式对于Array这种系统内置类型,想要进行二次修改可以通过使用该模式。
  2. 只是包装了一下扩展Array类,本质上与在函数体外面写相同,返回的对象与构造函数或与构造函数的原型属性之间并无关系。

稳妥构造函数模式

1
2
3
4
5
6
7
8
9
10
function Person(name) {
let o = new Object();
o.sayName = function() {
console.log('AAA I\'m big Big ' + name);
};
return o;
}

let p = Person('Bob');
p.sayName();

运行结果

1
AAA I'm big BigBob

特点总结

  1. 在该模式下,创建Person对象的时候,没有使用new操作符。
  2. 一般在安全环境下(禁止this等操作符),适合使用稳妥模式。

借用构造函数

为了解决原型中包含引用类型值所带来的问题,开始出现了借用构造函数。即在子类型构造函数的内部调用超类构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
function SuperType() {
this.colors = ['red', 'blue', 'yellow'];
}
function SubType() {
SuperType.call(this);
}

let ins1 = new SubType();
ins1.colors.push('green'); // ['red', 'blue', 'yellow', 'green']

let ins2 = new SubType();
ins2.colors; // ['red', 'blue', 'yellow']

所以说,这是在即将创建的那个对象的环境下调用了SuperType构造函数,这样就会在SubType对象上执行所有SuperType的初始化代码。

组合继承

这种技术是通过将原型链和借用构造函数结束组合使用,这样既可以通过在原型上定义方法实现了函数的复用,又能够保证每个实例都有它自己的属性。

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', 'yellow'];
}

function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}

SubType.prototype = new SuperType();
SubType.prototype.contructor = SubType;
SubType.prototype.sayAge = function() {
console.log(this.age);
}

原型式继承

借助已有的对象创建新对象。

1
2
3
4
5
6
// 道格拉斯·克罗克
function object(o) {
function F(){}
F.prototype = o;
return new F()
}

现在object函数内部,创建了一个临时的构造函数F,并且将传入的对象作为该构造函数的原型,最后返回这个临时类型的新实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let person = {
name: "Nicholas",
friends: ['Shelby', 'Court', 'Van']
}

let anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');

let yetAnotherPerson = object(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');

person.friends; // console.log : "Shelby, Court, Van, Rob, Barbie"

上述代码中,我们将person对象使用object函数作为参数传入,新创建的xanotherPerson对象将作为person对象的原型。

在ES5中,新增了Object.create() 方法规范化了原型式继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
let person = {
name: "Nicholas",
friends: ['Shelby', 'Court', 'Van']
};
let p1 = Object.create(person);
p1.name = 'Greg';
p1.friends.push('Hans');

let p2 = Object.create(person);
p2.name = "Shely";
p2.friends.push('Bob');

console.log(person.friends); // Shelby, Court, Van, Hans, Bob

对于Objet.create()方法还存在第二个参数(选填),写法与defineProperties()一致。

1
2
3
4
5
6
7
8
9
10
let person = {
name: "Nicholas",
friends: ['Shelby', 'Court', 'Van']
};

let anotherPerson = object.create(person, {
name: {
value: "Greg"
}
});

寄生式继承

该模型与原型式是紧紧相连的一种模式,他的实现类似于与寄生构造函数和工厂模式类似的。

1
2
3
4
5
6
7
function createAnother(original) {
let clone = object(original);
clone.sayHi = function() {
console.log('Hi');
}
return clone;
}

评论