JavaScript之面向对象编程

一直以来,都没有把JavaScript的面向对象编程这一块好好总结一下,今天就好好整理一下吧。

创建对象/类

JavaScript中的原型

每一个函数都有一个prototype(原型)属性,这个属性是一个指针,指向函数的原型对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。在默认下情况下,所有的原型对象都会都会自动获得一个constructor属性,这个属性包含一个指向prototype属性所在函数的指针。我们可以使用isPrototypeOf()方法,Object.getPrototypeOf()方法来确定对象与原型之间的关系。用ES6中的Object.setPrototypeOf()方法设置一个指定的对象的原型(即, 内部[[Prototype]]属性)到另一个对象或null。

1
2
3
4
5
6
7
8
9
10
function Person(){}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
alert(this.name);
};
let person1 = new Person();
Person.prototype.isPrototypeOf(person1);//true
Object.getPrototypeOf(person1) === Person.prototype;//true

如果在实例对象中创建了一个与原型对象中相同的属性名时,则会覆盖掉原型中的那个属性值,除非用delete删除实例属性,才会恢复对原型同名属性的连接。在这里,我们可以用hanOwnProperty()方法来检测一个属性是存在于实例中还是存在于原型中,存在实例中会返回true。而in操作符,无论是单独使用还是在for-in循环中使用,会在对象能够访问给定属性时返回true,无论属性存在于原型还是在实例中。

1
2
3
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && (name in object);
}

因此上面的这个函数我们可以确定出此属性到底是在原型中还是在实例中,true可以确定是在原型中,false表明是在实例中。for-in循环返回的是所有能够通过对象访问的,可枚举的属性。ES5将constructor和prototype属性的Enumerable特性设置为false。ES5中的Object.keys()方法也可返回所有可枚举属性。如果要想得到所有的属性,可以使用Object.getOwnPropertyNames()方法。

更简洁的语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(){}
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName: function() {
console.log(this.name);
}
};
//重设构造函数,只适用于ECMAScript5兼容的浏览器
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
Value: Person
});

注意:prototype对象重写之后constructor属性不再指向Person了,指向Object构造函数了,如果constructor属性真的很重要的话,可以用Object.defineProperty(),并且不会改变它的Enumerable值。
缺点:还是由其共享的本性所引起的,一个实例属性值改变,将会导致所有实例属性值改变。

基于原型的构造函数创建对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ['a', 'b'];
}
Person.prototype = {
constructor: Person,
sayName: function() {
console.log(this.name);
}
};
let person1 = new Person('Nicholas', 29, 'Software Engineer');
let person2 = new Person('Nixh', 20, 'Doctor');
person1.friends.push('c');
console.log(person1.friends); //['a', 'b', 'c']
console.log(person2.friends);//['a', 'b']

使用new操作符调用构造函数时会经历以下四个步骤:

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

new操作符创建系统内置对象

1
2
3
let obj1 = new Object();
let obj2 = new Array();
let obj3 = new Date();

Object.create()方法

语法:Object.create(proto[, propertiesObject]);

1
2
3
4
5
let obj1 = Object.create({
x: 1,
y: 2
});
let obj2 = Object.create(null);//创建一个原型为null的空对象

如果proto参数不是null或一个对象,则抛出一个TypeError异常。

字面量方式

1
2
3
4
5
6
7
let obj1 = {
name: 'dreams',
age: 22,
say: function() {
console.log('my name is ' + this.name);
}
}

ES6语法

1
2
3
4
5
6
7
8
9
10
11
12
class Point {
constructor (x, y) {
this.x = x;
this.y = y;
}
toString() {
return '('+this.x + ", " + this.y + ')';
}
}
typeof Point;//function
Point.prototype.constructor === Point;//true
Object.keys(Point.prototype);//[] 类内部定义的所有方法都是不可枚举的。这一点与ES5的行为不一致。

继承

JavaScript是基于原型的继承方式,为什么这么说呢?是因为在JavaScript中,每个对象都有一个proto(隐式原型) 的属性(Function多一个prototype(显式原型)属性),属性值指向创建这个对象的函数的显式原型(prototype),该原型对象又具有proto属性,层层向上直到对象Object的原型为null(Object.getPrototypeOf(Object.prototype) === null; // true),即对象原型链的终点为null,obj.proto.proto…的原型链由此产生。

ES5中的原型继承

首先看第一个例子:

经典继承

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
function Person(name, age) {
this.name = name || "person";
this.age = age || "20";
}
Person.prototype.getName = function(){
return this.name;
}
function Student(name, age, address) {
Person.call(this, name, age);
this.address = address || "China";
}
Student.prototype = Object.create(Person.prototype, {
constructor: {
value: Student,
enumerable: false,
configurable: true,
writable: true
},
toString: {
value: function() {
return "My name is " + this.name + ", age is " + this.age + ", address is " + this.address;
}
}
});
//Student.prototype.constructor=Student;
var Bob = new Student('Bob', 18, 'United States');
Bob.getName();//Bob
Bob.toString();//My name is Bob, age is 18, address is United States
Bob instanceof Student;//true
Bob instanceof Person;//true
console.dir(Bob);

dir输出

1
2
3
4
5
6
7
8
9
10
11
Student
|-- address: "United States"
|-- age: "18"
|-- name: "Bob"
|-- __proto__: Person
|-- constructor: ƒ Student(name, age, address)
|-- toString(): ƒ ()
|-- __proto__:
|-- getName: ƒ ()
|-- constructor: ƒ Person(name, age)
|-- __proto__: Object

这种方法主要是利用apply()或者是call()方法,在将要新创建的子类实例环境下调用父类构造函数。然后在利用ES5 增加的Object.create方法将Person.prototype加入原型链。由于Object.create()方法是ES5中的方法,不能保证所有的浏览器都会支持,因此我们可以通过一个空函数将父类的prototype加入到子类的原型链中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var extend = function(Base) {
var Class = function() {
Base.apply(this, arguments);
},F;
if(Object.create) {
Class.prototype = Object.create(Base.prototype);
} else {
F = function(){};
F.prototype = Base.prototype;
Class.prototype = new F();
}
Class.prototype.constructor = Class;
return Class;
}

这段代码来自于JavaScript编程指南一书的基于原型的JavaScript继承那一节。链接地址

原型链继承

1
2
3
4
5
6
7
8
9
10
11
function Super(){}
Super.prototype.fun1 = function(){}
function Sub(){}
Sub.prototype = new Super();
Sub.prototype.fun1 = function(){} //便可实现重载。
Sub.prototype.fun2 = function(){}
Sub.prototype.constructor = Sub;
var sub = new Sub();
console.log(sub.__proto__ == Sub.prototype);//true
console.log(Sub.prototype.__proto__ == Super.prototype);//true
console.dir(sub);//sub.constructor = Sub, fun1(), fun2()

这种方法主要是通过将父类的实例赋值给子类的prototype上来以此实现的。

ES6继承

主要是通过增加class与extends来实现继承的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person{
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class Student extends Person{
constructor(name, age) {
super(name);//调用父类的constructor(name)
this.age = age;
}
getAge() {
return super.getName() + '\'s age is ' + this.age;
}
}
let Bob = new Student('Bob', 22);
console.log(Bob.getName());
console.log(Bob.getAge());//Bob's age is 22

有过C++,或者java基础的相信不难理解。这里,我稍作解释一下:子类必须要在constructor方法中调用super方法,否则新建实例时会报错,这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。无论子类有没有显示定义constructor方法,都会存在这个方法。

虽然ES6的继承看起来跟真正的面向对象的编程语言很相像,但是JavaScript还是基于原型的继承,这一点并没有改变。