大家好,我们一起重温JavaScript。上一次我们一起重温了原型的相关内容,我们来一张导图回顾一下上次的内容吧:
你是否还记得上次的内容呢?点击此链接可以查看复习上次的内容:重温JavaScript(lesson11):面向对象(4)
这一次我们要在原型的基础上更进一步的学习有关JS面向对象的内容,首先看一下我们熟悉的在构造函数中使用原型对象的相关内容:
1.在原型上定义引用类型数据属性
首先我们要知道:某个对象可以共享其原型对象的属性。在上次分享中我们已经见到好几个这样的例子了,我们再来看一个不一样的例子:
function Person(name) {
this.age = 18;
this.name = name
}
Person.prototype.sayName = function() {
console.log(this.name);
}
var person1 = new Person("New_Name");
var person2 = new Person("重温新知");
person1.sayName();
//New_Name
console.log(person1.age);
//18
person1.sayName();
//重温新知
console.log(person2.age);
//18
在本例中,sayName()是一个原型属性,在用person1和person2调用sayName()时,相应的this的值被分别赋上person1或者person2。当使用二者去引用age属性时,所引用的都是自己的age,都是18。再看代码:
function Person(name) {
this.age = 18;
this.name = name
}
Person.prototype.sayName = function() {
console.log(this.name);
}
var person1 = new Person("New_Name");
var person2 = new Person("重温新知");
console.log(person1.age);
//18
console.log(person2.age);
//18
person1.age = 19;
console.log(person1.age);
//19
console.log(person2.age);
//18
这里我们修改了person1的age,对person2没有产生影响,因为它们分别拥有自己的属性。但是如果把某个引用类型的数据属性写在了原型上,那么这个属性就可以被多个对象实例所共有了,我们看代码:
function Person(name) {
this.name = name
}
Person.prototype.sayName = function() {
console.log(this.name);
}
Person.prototype.age = 18;
var person1 = new Person("New_Name");
var person2 = new Person("重温新知");
console.log(person1.age);
//18
console.log(person2.age);
//18
person1.age = 19;
console.log(person1.age);
//19
console.log(person2.age);
//18
在这段代码中,我们把age属性设置在构造函数Person的原型上,它变成被person1和person2这两个对象所共有。当person1改变age时,person2的age属性并没有发生变化。为什么呢,因为age是一个基本的数据类型数据,当执行person1.age = 19时,是为person1自己又单独分配了一个age属性,并没有改变原型的属性。我们打印出person1和person2对象:
通过上图我们看到:person1有自己的age属性,并且它的原型上也有age属性;person2没有age属性,它的原型上有age属性。我们接着看代码:
function Person(name) {
this.name = name
}
Person.prototype.sayName = function() {
console.log(this.name);
}
Person.prototype.hobby = ["吃饭","睡觉","写代码"];
var person1 = new Person("New_Name");
var person2 = new Person("重温新知");
console.log(person1.hobby);
//["吃饭", "睡觉", "写代码"]
console.log(person2.hobby);
//["吃饭", "睡觉", "写代码"]
person1.hobby.push("打游戏");
console.log(person1.hobby);
//["吃饭", "睡觉", "写代码", "打游戏"]
console.log(person2.hobby);
// ["吃饭", "睡觉", "写代码", "打游戏"]
如上代码所示:hobby属性是一个引用类型,被定义在原型对象上,这意味着person1.hobby和person1.hobby指向了同一数组。
我们注意到我们给原型添加属性的时候使用了 Person.prototype.sayName = 一个方法;Person.prototype.hobby = 一个数组;这样的形式。这属于在原型对象上一个一个地添加属性。既然原型是一个对象,我们就可以使用字面量的形式定义它可以有哪些属性。
2.使用字面量形式定义原型的属性
我们先来看一个例子:
function Person(name) {
this.name = name
}
Person.prototype = {
hobby: ["吃饭","睡觉","写代码"],
sayName: function(){
console.log(this.name);
}
}
var person1 = new Person("New_Name");
var person2 = new Person("重温新知");
console.log(person1.hobby);
// ["吃饭", "睡觉", "写代码"]
console.log(person2.hobby);
// ["吃饭", "睡觉", "写代码"]
person1.sayName();
//New_Name
person2.sayName();
//重温新知
这种方式和一个一个的定义原型的属性貌似没有什么分别,但还是有区别的,从表面上看这种方式更好,因为它不用多次键入Person.prototype,但是这么做是有副作用的,我们通过对比来看看:
2.1字面量形式的副作用
function Person(name) {
this.name = name
}
Person.prototype.hobby = ["吃饭","睡觉","写代码"];
Person.prototype.sayName = function(){
console.log(this.name);
}
var person1 = new Person("New_Name");
console.log(person1 instanceof Person);
//true
console.log(person1.constructor === Person);
//true
console.log(person1.constructor === Object);
//false
这种方式下,对象的构造函数指向了Person函数,或者说对象的原型的构造器指向了Person函数。因为根据我们上一次lesson学过的内容使用Person.prototype.constructor和person1.constructor是等效的。我们再来看使用字面形式的:
function Person(name) {
this.name = name
}
Person.prototype = {
hobby: ["吃饭","睡觉","写代码"],
sayName: function(){
console.log(this.name);
}
}
var person1 = new Person("New_Name");
console.log(person1 instanceof Person);
//true
console.log(person1.constructor === Person);
//false
console.log(person1.constructor === Object);
//true
我们发现,使用对象字面形式改写原型对象改变了构造函数的属性,因此它现在指向Object而不是Person。为什么呢?这是因为原型对象具有一个constructor属性,这是其他对象实例所没有的。当一个函数被创建时,它的prototype属性也被创建,且该原型的constructor属性指向该函数。这是我们上一次lesson中重点讲解的内容。而当使用对象字面量的形式改写原型对象Person.prototype时,constructor属性将会被置为Object。这就是使用字面形式定义原型的一个副作用。
如何避免这样的副作用呢?我们可以在改写原型对象时手动设置其constructor属性
2.2设置原型对象constructor属性
如下例:
function Person(name) {
this.name = name
}
Person.prototype = {
constructor: Person,
hobby: ["吃饭","睡觉","写代码"],
sayName: function(){
console.log(this.name);
}
}
var person1 = new Person("New_Name");
console.log(person1 instanceof Person);
//true
console.log(person1.constructor === Person);
//true
console.log(person1.constructor === Object);
//false
在如上代码中,我们显示地指定了原型对象的constructor属性。如果我们以后要用字面量的形式改写对象的原型,为了不忘记赋值,最好把它设为原型对象的第一个属性。
另外如果你使用了对象字面量的形式定义原型之后,如果你还想往原型上添加属性,那么你完全可以继续使用prototype,如下代码:
function Person(name) {
this.name = name
}
Person.prototype = {
constructor: Person,
hobby: ["吃饭","睡觉","写代码"],
sayName: function(){
console.log(this.name);
}
}
var person1 = new Person("New_Name");
var person2 = new Person("重温新知");
Person.prototype.sayHi = function() {
console.log("Hi");
}
person1.sayHi();
//Hi
person2.sayHi();
//Hi
这段代码中,在创建了两个Person实例之后,又给原型对象添加了方法,则这两个对象任然能使用这个方法。那如果对象字面量和构造函数.prototype这两种方式反过来写呢?
2.3对象和原型引用关系的建立时机
我们看例子:
function Person(name) {
this.name = name
}
Person.prototype.sayHi = function() {
console.log("Hi");
}
Person.prototype = {
constructor: Person,
hobby: ["吃饭","睡觉","写代码"],
sayName: function(){
console.log(this.name);
}
}
var person1 = new Person("New_Name");
person1.sayHi();
//报错 TypeError: person1.sayHi is not a function
根据上次lesson我们说过的三点重要内容你会知道:对象和函数原型之间的引用关系是在对象创建时建立的。此时person1是在新的原型字面量覆盖旧的原型之后创建的,所以只能访问新的原型所拥有的属性,不能访问sayHi方法。我们再来看,如果把person1的创建放在定义新的原型之前会有什么结果?
function Person(name) {
this.name = name
}
Person.prototype.sayHi = function() {
console.log("Hi");
}
var person1 = new Person("New_Name");
Person.prototype = {
constructor: Person,
hobby: ["吃饭","睡觉","写代码"],
sayName: function(){
console.log(this.name);
}
}
person1.sayHi();
//Hi
person1.sayName();
//Uncaught TypeError: person1.sayName is not a function
在这段代码中,尽管在创建person1之后又修改了原型,但是对象和函数原型之间的引用关系是在对象创建时建立的,这并不影响person1。同理,即使之后的原型对象中添加了sayName方法,但是对person1而言,它的原型是创建时确立的,并没有sayName方法,所以不能访问sayName方法。我们可以打印person1的原型和Person函数的原型对比看一下:
function Person(name) {
this.name = name
}
Person.prototype.sayHi = function() {
console.log("Hi");
}
var person1 = new Person("New_Name");
Person.prototype = {
constructor: Person,
hobby: ["吃饭","睡觉","写代码"],
sayName: function(){
console.log(this.name);
}
}
console.log(Object.getPrototypeOf(person1));
//{sayHi: ƒ, constructor: ƒ}
console.log(Person.prototype);
//{hobby: Array(3), constructor: ƒ, sayName: ƒ}
二者的原型确实是不一样的,所以我们要记住:对象和函数原型之间的引用关系是在对象创建时建立的。new一个对象的时候,构造函数的原型会被赋给该对象的[[Prototype]]属性。
到这里我们来做个小结:构造函数、原型对象、对象实例之构造间的关系是非常微妙的。对象实例和构造函数没有直接联系,而是通过原型对象联系的。可以通过如下图示来理解:
感觉这次内容挺多的,有关继承的内容就放在下次说吧,以上就是我们这次分享的主要内容,来张导图总结一下吧: