JavaScript中的面向对象

对象的定义

无序属性的集合,其属性可以包含基本值、对象或函数。可以将对象想象成散列表,即一组名值对,值可以是数据或是函数

属性设置

采用Object.defineProperty() 方法直接在一个对象上定义一个新属性,或者修改一个已经存在的属性,并返回这个对象。

Object.defineProperty(obj, prop, descriptor)

  • obj 需要定义属性的对象。
  • prop 需被定义或修改的属性名。
  • descriptor 需被定义或修改的属性的描述符。

属性主要分为两种: 数据属性和访问器属性

configurable: 当且仅当该属性的configurable为true时,该属性才能够被改变,也能够被删除,默认为false。

enumerable: 当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中,默认为false。

value: 该属性对应的值。可以是任何有效的JavaScript值(数值,对象,函数等),默认为undefined。

writable: 当且仅当该属性的writable为true时,该属性才能被赋值运算符改变,默认为false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var person = {
age: 11,
sex: 'man'
}
Object.defineProperty(person, 'height', {
value: 198
});
for(var v in person){
console.log(v); // age, sex, 体现enumerable
}
person.height = 3;
console.log(person.height); // 198, 体现writable
delete person.height;
console.log(person.height); // 198, 体现configurable

get: 一个给属性提供getter的方法,如果没有getter则为undefined。该方法返回值被用作属性值。默认为undefined。

set: 一个给属性提供setter的方法,如果没有setter则为undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为undefined。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
console.log(book.edition);
for(var attr in book){
console.log(attr); // _year, edition
}

构造函数

调用构造函数会经历的四个步骤,此前在this的文章中已经讲过,这里再复习一下:

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

看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name, age) {
this.name = name;
this.age = age;
this.getName = function() {
console.log(this.name);
}
}
var person1 = new Person("Tom", 12);
console.log(person1.constructor == Person); // true
console.log(person1 instanceof Object && person1 instanceof Person); // true
var person2 = new Person("Jom", 13);
console.log(person1.getName == person2.getName); // true

原型模式

使用构造函数模式,我们发现构造函数中的每个对象方法都要在实例上重新创造一遍,都new Function(),可用原型模式解决,它的好处在于可以让所有实例共享它包含的属性与方法.

与原型有关的重要方法

isPrototypeOf()

object1.isPrototypeOf(object2)

object1是否在object2的原型链中

Object.getPrototypeOf()(ES5)

Object.getPrototypeOf(object)

返回指定方法的原型

Object.prototype.hasOwnProperty()

obj.hasOwnProperty(prop)

用来判断某个对象是否含有指定的自身属性

in

in操作符可以单独使用或在for-in循环中使用,无论属性存在于实例还是原型中,但必须为可遍历的

Object.keys()(ES5)

返回一个由给定对象的所有可枚举自身属性的属性名组成的数组

Object.getOwnPropertyDescriptor()

Object.getOwnPropertyDescriptor(obj, prop)

返回指定对象上一个自有属性对应的属性描述符

多个实例共享所保存的属性和方法的基本原理:当读取某个对象的某个属性时,都会进行一次搜索,先从本身实例开始,若在实例中找到改名字的属性,则返回,若没有找到,则继续搜索,指针指向原型对象,但是要注意,这样可能会产生实例对象屏蔽原型对象的情况发生.

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
33
34
function Person(name, age) {
this.name = name;
this.age = age;
this.getAge = function() {
console.log(this.age + 5);
}
}
Person.prototype.getName = function() {
console.log(this.name);
}
Person.prototype.getAge = function() {
console.log(this.age);
}
var person1 = new Person("Tom", 12);
var person2 = new Person("Jom", 13);
console.log(person1.getName == person2.getName);
console.log(person1.__proto__); // Person()
console.log(person1.constructor); // function Person()
console.log(Person.prototype); // Person()
console.log(Person.prototype.constructor); //// function Person()
person1.getAge(); // 17而不是12
console.log(Person.prototype.isPrototypeOf(person2)); // true
console.log(Object.getPrototypeOf(person1)); // Person()
console.log(person1.hasOwnProperty("getAge")); // ture
console.log(person1.hasOwnProperty("getName")); // false
console.log("getName" in person1); // true
console.log(Object.keys(person1)); // ["name", "age", "getAge"]
console.log(Object.getOwnPropertyDescriptor(person1, "name")); // Object {value: "Tom", writable: true, enumerable: true, configurable: true}

重整原型函数,封装原型的功能,但会出现一些问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 组合使用构造函数模式和原型模式
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = {
getName: function() {
console.log(this.name);
},
getAge: function() {
console.log(this.age);
}
}
//这里会出现一个问题, constructor不指向Person
var person1 = new Person();
console.log(person1.constructor); // Object

解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
// ES6方法
Object.assign(Person.prototype, {
getName: function() {
console.log(this.name);
},
getAge: function() {
console.log(this.age);
}
})

继承

ECMAScript只支持实现继承,主要依靠原型链来完成

A需要继承B,既然A.prototype与B.prototype相连接

A.prototype = B.prototype 是完全错误,js是引用复制,后续扩展B.prototype对A产生影响

A.prototype = new B() 如果函数B有一些副作用,就会产生一些影响

`A.prototype = Object.create(B.prototype)` or ES6中,也可使用`Object.setPrototypeOf(A.prototype, B.prototype)`

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
33
34
35
36
function Person(name, age) {
this.name = name;
this.age = age;
this.test = function(){
console.log('test');
}
}
Person.prototype.getName = function(){
console.log(this.name);
}
function Student(score) {
this.score = score;
}
Student.prototype = Object.create(Person.prototype);
/* Object{} 只有getName
* 凭Person.prototype创建一个“新”对象并把它关联到Foo.prototype中
*/
console.log(Student.prototype);
Student.prototype = new Person();
/* Person {name: undefined, age: undefined}
* 这样Student上就会有name、age和test
*/
console.log(Student.prototype);
Student.prototype.getScore = function() {
console.log(this.score);
}
var stu = new Student('90');
stu.getScore(); // 90
// TypeError,如果是new Person() 则会输出'test'
stu.test();

如果Person函数中有赋值的变量,使用new Person()给Student的原型后就会发生共享,我们还需要借用构造函数来实现对实例对象的继承,即(寄生)组合继承模式,原型属性和方法通过原型链来继承,实例属性通过构造方法来继承:

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
33
function Person(name, age) {
this.name = name;
this.age = age;
this.getAge = function() {
console.log(this.age);
}
}
Person.prototype.getName = function(){
console.log(this.name);
}
function Student(score, name, age) {
// 继承属性, 其实相当于 this.name = name, this.age = age定义在这里,但getAge也会被call
Person.call(this, name, age);
this.score = score;
}
/*
* Student.prototype = new Person();
* 这里如果使用new的话,相当于调用了两次Person(),同时会使Student原型与实例都带有name,age属性,没有必要
*/
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student
Student.prototype.getScore = function(){
console.log(this.score);
}
var stu = new Student(59, 'Tom', 23);
stu.getName(); // Tom
stu.getScore(); // 59
stu.getAge(); // 23

总之原型链是这样的: 实例本身属性与方法(在构造函数中或自己定义赋值) -> 父类的原型属性与方法 -> 父类的父类的原型属性与方法 -> … -> object.prototype -> null。

除了本身的属性与方法外,其它都是要与其它实例共享的,如果改变原型链上的方法会影响其它实例,如果定义了与原型链一样的属性或方法,原型链上的方法将会被屏蔽。