构造函数
构造函数就是我们用new创建对象时调用的函数。到目前为止,我们已经学过好多内置的构造函数了,例如,Object
,Array
和Function
。使用同一个构造函数创建的对象都具有相同的属性和方法。除了这些内置的构造函数外,我们也可以创建自己的构造函数。
构造函数也是函数,和普通函数的定义方式一样。唯一的区别是构造函数名的首字母应该大写,以此区分于其他函数。下面定义了一个空的Person函数。
function Person() {
}
定义好构造函数以后,你就可以用它来创建对象了,例如,下面使用Person构造函数创建了2个对象。
var person1 = new Person();
var person2 = new Person();
当我们使用 new
操作符调用函数时,函数内部会自动创建一个该类型的新对象。当函数调用完成时,该对象会被自动的返回到函数外部。我们可以使用 instanceof
操作符检查 person1
和 person2
是否是 Person
类型的对象实例:
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Person); // true
由于person1
和person2
是通过Person
构造函数创建的,因此使用instanceof
操作符检查它们是否为Person
类型的实例时返回true
。
声明一个空的构造函数其实没有什么用。使用构造函数的目的是为了轻松创建许多拥有相同属性和方法的对象。我们可以在函数内部通过 this
关键字使用新创建的对象,如下例:
function Person(name) {
this.myName = name;
this.sayName = function() {
console.log(this.myName);
}
}
上面的Person
构造函数接受一个参数name
,并通过 this
将其赋值给新对象的 myName
属性。同时,构造函数还给新对象添加了一个 sayName()
方法。函数执行完毕时,会自动将新创建的对象返回到函数外部。
接下来我们就开始使用 Person
构造函数创建具有初始属性和方法的对象:
var person1 = new Person('张三');
var person2 = new Person('李四');
console.log(person1.myName); // '张三'
console.log(person2.myName); // '李四'
person1.sayName(); // '张三'
person2.sayName(); // '李四'
每个对象都有自己的myName
属性,所以sayName()
方法可以根据不同对象返回不同的值。
注意:你也可以在构造函数中显式调用
return
。如果返回的值是一个对象,它会代替新创建的对象实例返回。如果返回的值是一个原始类型,它会被忽略,新创建的对象实例会被返回。
原型对象
prototype属性
一个对象实例通过内部属性 __proto__
跟踪其原型对象。该属性的值是一个内存地址(指针),指向该实例使用(继承)的原型对象。当我们使用 new
创建一个新的对象时,构造函数的prototype
对象会被赋值给新对象的__proto__
属性。下图描绘了__proto__
属性的指向:
图1,
__proto__
属性的指向,person1和person2的__proto__
属性指向同一个原型对象
虽然我们可以直接使用 __proto__
属性获取对象的原型,但是不建议这样使用。
console.log(person1.__proto__)
我们可以使用 Object.getPropertyOf()
方法读取 __proto__
属性指向的原型对象。
// 获取 person1 和 person2 对象的原型对象
var prototype1 = Object.getPrototypeOf(person1);
var prototype2 = Object.getPrototypeOf(person2);
// 检查 person1 与 person2 的原型对象是否相等
console.log(prototype1 === prototype2); // true
// 检查 person1 和 person2 的原型对象是否等于 Person.prototype
console.log(prototype1 === Person.prototype); // true
console.log(prototype2 === Person.prototype); // true
说明上面的代码说明 person1
和 person2
的 __proto__
属性指向同一个对象,也就是Person.prototype
。
大部分JavaScript引擎在所有对象上都支持一个名为
__proto__
的属性。Firefox、Safari和Chrome都支持该属性。
我们也可以用isPrototypeOf()
方法检查某个对象是否是另一个对象的原型对象,该方法包含在所有对象中。
var obj = {};
console.log(Object.prototype.isPrototypeOf(obj)); // true
因为 obj
对象的原型是 Object.prototype
,因此 isPrototypeOf()
方法返回 true
。
原型对象链
由于原型对象也是一个对象,它也有自己的原型对象并继承原型对象中的属性。这就是原型对象链:对象继承其原型对象,而原型对象继承它的原型对象,依此类推。
当读取一个对象的属性时,JavaScript引擎首先在该对象的自有属性中查找属性名字。如果找到则返回。如果自有属性中不包含该属性,则JavaScript会沿着原型对象链搜索原型对象中的属性。如果找到则返回,否则返回undefined
。
改变原型对象
原型对象的共享机制使得它们成为一次性为所有对象定义方法的理想手段。因为一个方法对所有的对象实例做相同的事,没有必要每个实例都要有一份自己的方法。
将方法放在原型对象中并使用this
访问当前实例是更有效的做法。例如:
function Person(name) {
this.myname = name;
}
Person.prototype.sayName = function() {
console.log(this.myName);
}
var person1 = new Person('张三');
var person2 = new Person('李四');
console.log(person1.myName); // '张三'
console.log(person2.myName); // '李四'
person1.sayName(); // '张三'
person2.sayName(); // '李四'
上面的代码将sayName()
方法定义在原型对象上而不是构造函数中。创建出的对象和前面例子中的一样,只不过sayName()
现在是一个原型属性而不是自有属性。
虽然我们可以在原型对象上一个一个地添加属性,但是很多开发者会使用另一种更简洁的方式:直接用一个对象字面量形式替换原型对象,示例如下:
function Person(name) {
this.myname = name;
}
Person.prototype = {
sayName: function() {
console.log(this.myName);
},
toString: function() {
return '[Person ' + this.myname + ']';
}
};
上例中的代码在原型对象上定义了两个方法,sayName()
和toString()
。这种定义方式非常流行,但是有一个副作用需要注意。
var person1 = new Person('张三');
console.log(person1 instanceof Person); // true
console.log(person1.constructor === Person); // false
console.log(person1.constructor === Object); // true
使用对象字面量形式改写原型对象后改变了constructor
属性,因此它现在指向Object
而不是Person
。这是因为原型对象具有一个constructor
属性,这是其他对象实例所没有的。当一个函数被创建时,它的prototype
属性也被创建,且该原型对象的constructor
属性指向该函数。当使用对象字面量的形式改写原型对象Person.prototype
时,其constructor
属性也被改成了Object
。为了避免这一点,需要在改写原型对象时手动重置其为constructor
属性,示例如下:
function Person(name) {
this.myname = name;
}
Person.prototype = {
constructor: Person,
sayName: function() {
console.log(this.myName);
},
toString: function() {
return '[Person ' + this.myname + ']';
}
};
var person1 = new Person('张三');
var person2 = new Person('李四');
console.log(person1 instanceOf Person); // true
console.log(person1.constructor === Person); // true
console.log(person1.constructor === Object); // false
console.log(person2 instanceOf Person); // true
console.log(person2.constructor === Person); // true
console.log(person2.constructor === Object); // false
本例显式地指定了原型对象的constructor
属性。为了不忘记赋值,最好把它设置为原型对象的第一个属性。
内置对象的原型对象
所有内置对象都有构造函数,因此也都具有原型对象,下面是 Array 类型对象的原型对象链:
var arr = new Array(1, 2, 3, 4);
下面是 Object 类型对象的原型对象链:
var obj = {mName: '赵四'};
如果要给所有数组添加一个新方法只需要简单的修改 Array.prototype
即可。
例如,给数组添加一个inArray()
方法,用于判断数组中是否包含某个元素:
Array.prototype.inArray = function (ele) {
// 如果数组长度为0,直接返回false
if(!this.length) return false;
// 否则遍历数组元素,与参数ele比对,比对成功返回true
for (var i = 0; i < this.length; i++) {
if(this[i] === ele) {
return true;
}
}
return false;
};
var colors = ['red', 'green', 'blue'];
console.log(colors.inArray('red')); // true
console.log(colors.inArray('purple')); // false
继承
学习如何创建对象是理解面向对象编程的第一步。第二步是理解继承。在传统的面向对象语言中,类从其他类继承属性。然而在JS中,在没有类的情况下,继承可以发生在对象之间。这种继承的机制你已经熟悉了,就是继承原型对象中属性。
原型对象继承
JS内置的继承方式被称为原型对象链继承,又可称为原型对象继承。就像我们前面讲的那样,原型对象的属性可以被对象实例访问,这就是继承的一种形式。对象实例继承了原型对象中的属性。
所有对象,包括那些我们通过自定义构造函数创建的对象都自动继承自Object
,除非我们另有指定。更确切地说,所有对象都继承自Object.prototype
。任何以Object的字面量形式创建的对象,其__proto__
属性的值都被自动设置为Object.prototype
,这意味着它继承Object.prototype
的属性。
var book = {
title: '钢铁是怎样炼成的'
};
var prototype = Object.getPrototypeOf(book);
console.log(prototype === Object.prototype); // true
上面的代码表明book
的原型对象是Object.prototype
。这意味着book
会自动继承来自Object.prototype
的方法。
继承自Object.prototype的方法
我们在前面用的好几个方法其实都是定义在Object.prototype
上的。因此可以被其他对象继承。这些方法如下:
hasOwnProperty()
检查是否存在一个给定名字的自有属性propertyIsEnumerable()
检查一个自有属性是否可枚举isPrototypeOf()
检查一个对象是否是另一个对象的原型对象valueOf()
返回一个对象的值表达toString()
返回一个对象的字符串表达
这5个方法会被所有的对象继承。当需要让对象在JS中以一致的方式工作时,最后两个尤其重要,有时我们需要自己定义它们。
修改Object.prototype
所有对象都会继承Object.prototype对象的属性和方法。如下例所示,给Object.prototype添加方法:
Object.prototype.add = function (value) {
return this + value;
};
添加到Object.prototype对象上的属性会被所有类型的对象所继承,有些时候这样写是没有意义的,并且还可能会出错。不要轻易修改Object.prototype对象。
var obj1 = {
myName: '李四'
};
var arr1 = [1, 2, 3, 4];
var date1 = new Date();
console.log(obj1.add(2)); // '[object Object]2'
console.log(arr1.add(3)); // '1,2,3,43'
console.log(date1.add(4)); // 'Wed May 02 2018 16:21:01 GMT+0800 (中国标准时间)4'
如果我们想要让一些属性或方法被所有共享,可以使用继承实现。
对象继承
对象继承是最简单的继承方式。我们唯一需要做的就是指定哪一个对象是新对象的原型对象。对象字面量形式会隐式指定Object.prototype
为其原型对象,我们也可以使用Object.create()
方法显式指定原型对象。
Object.create()
方法接受两个参数。第一个参数是需要被设置为新对象的原型对象。第二个参数是选的,用来为新对象添加属性,如下所示:
var book = {
title: '平凡的世界'
};
// 等价于下面的代码
var book = Object.create(Object.prototype, {
title: {
configurable: true,
enumerable: true,
value: '平凡的世界',
writable: true
}
});
两种声明具有相同的效果,第一种声明使用对象字面量形式来定义一个具有单一属性title
的对象。该对象自动继承自Object.prototype
,且其属性被默认设置为可配置、可枚举和可写。第二种声明使用Object.create()
方法显式做了同样的操作。两个book
对象的行为完全一致。但这样写没啥意义,使用字面量定义的对象默认就继承Object.prototype
,因此我们没有必要这样写。继承自其他对象则更有趣,如下所示:
var person1 = {
name: '张三',
sayName: function() {
console.log(this.name);
}
}
var person2 = Object.create(person1, {
name: {
configurable: true,
enumerable: true,
value: '李四',
writable: true
}
});
person1.sayName(); // '张三'
person2.sayName(); // '李四'
console.log(person1.hasOwnProperty('sayName')); // true
console.log(person1.isPrototypeOf(person2)); // true
console.log(person2.hasOwnProperty('sayName')); // false
这段代码首先创建了一个对象person1
,该对象有一个name
属性和一个sayName()
方法。对象person2
继承自person1
,也就继承了name
属性和sayName()
方法。然而在通过Object.create()
方法创建person2
对象时也定义了一个自有属性name
。该自有属性遮挡了原型对象中的同名属性。所以,person1.sayName()
输出'张三'
,而person2.sayName()
输出'李四'
。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mTWzuQUl-1682416367288)(E:\html11\04_第二阶段\02_JS面向对象\day05_继承\jichenglian.png)]