学习 JavaScript 中的原型对象这一篇就够了
学习内容:
1、 理解什么是构造函数 2、 理解什么是原型对象 3、 理解构造函数、实例对象与原型对象之间的关系 4、 掌握原型对象中定义属性和方法的方法 5、掌握原型对象相关的属性和方法 今天我们来学习一下js中的原型对象,这是js面向对象中的一大难点也是面试最容易问到的问题。在学习的时候博主也在网上查找了大量的资料,发现了一个对于初学者最致命的问题,就是好多文章在讲解该内容的时候都是使用特别专业的术语来讲解的,导致很多初学者都听不懂或者理解不了,可能会导致大家走了很多的弯路,那么在本片文章博主将使用通俗易懂的方式来像大家介绍。
一、什么是构造函数?
构造函数实际上就是一个特殊的函数,为什么说特殊呢,因为它是初始化函数。如果通过操作符new
来调用的函数就是构造函数,否则就是普通函数。而在Javascript的类(class)中,使用constructor
关键字来声明的函数被称为构造函数或初始化函数。在通过new
操作符调用或实例化对象的时候,默认会去执行该构造函数,所有我们称它为初始化函数。
// ES5中这是一个构造函数,是创建对象的一种方式
function Father(){
}
// new操作符调用Father构造函数,并返回一个实例对象
var father = new Father();
// ES6中使用class(类)创建对象
class Father{
constructor(){
}
}
var father = new Father();
那么,在这里我们可以总结一下操作符new
的用处,其一,调用构造函数
,其二,同时返回一个实例对象
。
二、什么是原型对象
原型:
是JavaScript中继承
的基础,E36之前JavaScript中的继承就是就是基于原型的继承。
2.1 构造函数的原型对象
原型对象:
在通过function关键字
定义一个构造函数
或者通过class关键字
定义一个类
之后,浏览器会按照一定的规则在内存中自动创建一个对象(注意:并非是我们通过new操作符而创建的对象),那么这个对象就叫做原型对象。
那么,在我们定义的构造函数,或者通过class类
定义的构造函数中默认会有一个prototype
属性,这个属性指向的就是该构造函数对应的原型对象,而原型对象中会默认拥有一个constructor
属性,这个属性指向的就是这个构造函数。
// 构造函数
function Father() {
// 实例属性
this.name = name;
// 实例方法
this.run = function() {
console.log(`我叫${this.name},我会跑!`)
}
}
// 实例化对象
var father = new Father("张三");
// 打印构造函数的原型对象
console.log(Father.prototype);
2.3 实例对象
1. 当我们通过构造函数(理论上任何函数都可以作为构造函数)使用new操作符创建对象的时候,创建的这个对象我们称之为实例对象
,而实例对象默认拥有一个隐式的__proto__
属性,实例对象调用该属性可以直接访问到所对应构造函数的原型对象。
2. 那么,也就是说,实例对象的__proto__
属性指向的是所对应构造函数的原型对象。那么原型中的constructor
属性的作用就是用于记录该对象引用于哪个构造函数,它可以让原型对象再次指向对应构造函数。
// 构造函数
function Father() {
// 实例属性
this.name = name;
// 实例方法
this.run = function() {
console.log(`我叫${this.name},我会跑!`)
}
}
// 实例化对象
var father = new Father("张三");
// 打印实例对象所对应构造函数的原型对象,可以知道该对象来自于哪个构造函数
console.log(father.__proto__);
// 判断实例对象的构造函数的原型对象是否全等于构造函数的原型对象
console.log(father.__proto__ === Father.prototype);
3. 如果创建了多个实例对象,则多个实例对象都会指向对应构造函数的原型对象。那么我们可以给这个原型对象添加属性和方法,所有的实例对象就会共享原型对象中添加的属性和方法。
// 构造函数
function Father() {
// 实例属性
this.name = name;
// 给Father构造函数的原型对象添加属性
Father.prototype.age = 20;
// 实例方法
this.run = function() {
console.log(`我叫${this.name},我会跑!`)
}
}
// 实例化对象
var father = new Father("张三");
var person = new Father("李四");
// 判断两个实例对象所对应构造函数的原型对象是否全等
console.log(father.__proto__ === person.__proto__); //true
// 判断两个实例对象对应构造函数的原型对象的属性是否一致
console.log(father.name === person.name); //true
4. 访问实例对象那个中的一个属性,假如在实例对象中找到,则直接返回;如果没有找到,则直接去实例对象的__protp__
属性所指的原型对象中查找,查找到则返回,否则继续向上找原型对象的原型—原型链。
// 构造函数
function Father(name) {
// 实例属性
this.name = name;
// 给Father构造函数的原型对象添加属性
Father.prototype.age = 20;
// 实例方法
this.run = function() {
console.log(`我叫${this.name},我会跑!`)
}
}
// 实例化对象
var father = new Father("张三");
// 打印实例对象的属性age
console.log(father.age); //返回20,即原型对象的属性
// 1、可以看出实例对象没有age属性,而原型对象中有
// 2、那么先去在实例对象中查找,没找到所以找到了实例对象中对应的属性
5. 如果实例对象中添加了该属性,则对于实例对象来说就屏蔽了原型对象中的对应属性。简单来说,假如在实例对象中找到,就没办法访问原型对象中的属性。
// 构造函数
function Father(name) {
// 实例属性
this.name = name;
// 实例对象的age
this.age = 10;
// 给Father构造函数的原型对象添加属性
Father.prototype.age = 20;
// 实例方法
this.run = function() {
console.log(`我叫${this.name},我会跑!`)
}
}
// 实例化对象
var father = new Father("张三");
// 打印实例对象的属性age
console.log(father.age); //返回10,即实例对象的属性
// 1、可以看出实例对象有age属性,并且原型对象中也有
// 2、那么实例对象的属性就会覆盖原型对象的属性
**重点注意:**通过实例对象只能访问(读取)原型对象中的属性,而不能修改原型对象中的属性值。如果以实例对象.属性名=属性值
的方式添加属性,则将添加到实例对象中,而并非原型对象。
// 构造函数
function Father(name) {
// 实例属性
this.name = name;
// 给Father构造函数的原型对象添加属性
Father.prototype.age = 20;
// 实例方法
this.run = function() {
console.log(`我叫${this.name},我会跑!`)
}
}
// 实例化对象
var father = new Father("张三");
// 试图修改原型中的属性age
father.age = 8;
// 结果发现输出的是8
console.log(father.age);
// 1、首先如果没有 father.age = 8; 则输出的是20,这没问题
// 2、但问题就出现在这行code上,由于它没有修改原型对象中的age属性,而是给实例对象添加了一个同名的属性
// 3、此时添加的age实例属性将覆盖原型对象的属性age,所以输出的是8
2.4 构造函数存在的问题
**问题:**构造函数很好用,但是存在浪费内存的问题。也就是说每实例一次对象就就在内存中开辟了一次空间。所以我们希望所有的对象都使用同一个函数,而不去重新分配空间,这样就比较节省内存的消耗,那么我们如何实现呢?
**解决:**说到这里,我们回过头来看原型对象,它有一个特点就是多个实例对象可以共享原型的属性和方法,那么我们是不是就可以通过原型对象来解决这个问题呢?
一般情况下,对象的方法或属性会在构造函数,或者构造函数外类内(class)书写。但是如果有多个对象要使用该方法,我们可以给构造函数的原型对象采取对象的形式赋值,这样所有实例的对象都能够去访问。
但是这样会覆盖构造函数原型对象原来的内容,这样修改后的原型对象的constructor
属性就不再指向当前的构造函数了。那么此时,我们可以在修改后的原型对象中,添加一个constructor
属性指向原来的构造函数。
// 构造函数创建对象
function Person(name, age) {
this.name = name;
this.age = age;
};
//获取Person的原型对象,并以对象的形式赋值,注意:必须放到构造函数外,否则抛异常
Person.prototype = {
// 手动设置,指回原来的构造函数
constructor: Person,
sex: "男",
speak: function() {
console.log(`我的名字是${this.name},我会说话!`)
},
run: function() {
console.log(`我今年${this.age}岁了,我会跑!`)
}
};
// 实例化调用
var father = new Person("父亲", 40);
father.speak();
father.run();
let son = new Person("儿子", 18);
son.speak();
son.run()
console.log(father.__proto__ === son.__proto__) //返回true
注意:如果使用class(类)的方式创建对象,则不能使用以对象的形式给原型进行赋值,则必须以类名.prototype.属性名/方法名
的方法赋值。书写的位置可以在构造函数内,类外,但是不能在类内构造函数外,否则抛异常
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
// 原型对象的属性和方法
Person.prototype.sex = "男";
Person.prototype.speak = function() {
console.log("我的名字叫:" + this.name);
};
Person.prototype.run = function() {
console.log("我今年" + this.age + "岁了");
};
}
}
// 实例化调用
var father = new Person("父亲", 40);
father.speak();
father.run();
let son = new Person("儿子", 18);
son.speak();
son.run()
console.log(son.sex)
console.log(father.__proto__ === son.__proto__) //返回true
三、关于原型对象常用的方法和属性
1、prototype 属性
prototype
属性存在于构造函数中(其实任意函数中都有该属性,只不过普通函数的该属性我们没有必须关注而已),它指向了构造函数的原型对象。相信大家在上面的解释和案例中已经对该属性有了一定的了解,那么在这里就不做过多的解释。
2、constructor 属性
contructor
属性存在于原型对象中,它指向了构造函数,也就是说原型对象的该属性记录了构造函数的引用,所以指向了构造函数。
3、__proto__
属性
用构造函数创建一个新对象之后,这个对象默认会拥有一个不可访问的属性prototype
,这个属性就指向了构造函数的原型对象。
而在个别浏览器中也提供了对这个属性prototype
的访问,即通过__proto__
属性来访问,但是我们尽量不要使用这种方式去访问,因为操作不慎会改变这个对象的继承原型链。所以在vscode中使用该属性是没有提示的。
4、hasOwnProperty( )方法
大家知道,我们在访问一个对象的属性时,这个属性既有可能来自对象本身,也有可能来自这个对象__proto__
属性所指向的原型对象,那么我们如何判断该属性的来源呢?
那么这里,我们就可以使用hasOwnProperty
方法来判断该属性是否来自对象本身。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
// 原型对象的属性和方法
Person.prototype.sex = "男";
Person.prototype.speak = function() {
console.log("我的名字叫:" + this.name);
};
Person.prototype.run = function() {
console.log("我今年" + this.age + "岁了");
};
}
}
// 实例化调用
var father = new Person("父亲", 40);
// 返回true,因为name属性来自于对象本身
console.log(father.hasOwnProperty("name"));
// 返回false,因为它来自于对象__proto__属性所指向的原型对象
console.log(father.hasOwnProperty("sex"));
注意:通过hasOwnProperty()
方法可以判断一个属相是否在实例对象自身添加的,但是不能判断是否存在于原型对象中,因为有可能判断的这个属性压根不存在。那么也就是说在原型对象中的属性和不存在的属性都会返回false
。
5、in 操作符
上面我们知道了hasOwnProperty()
方法的弊端,但是有时候,我们需要知道该属性是否存在于原型对象中,那么该怎么办呢?
in操作符
用来判断一个属性是否存在于这个对象中。但是在查找这个属性的时候,先在对象自身找,如果找不到再去原型对象找。换句话说,只要对象和原型中有一个地方存在这个属性,就返回true
,否则就返回false
。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
// 原型对象的属性和方法
Person.prototype.sex = "男";
Person.prototype.speak = function() {
console.log("我的名字叫:" + this.name);
};
Person.prototype.run = function() {
console.log("我今年" + this.age + "岁了");
};
}
}
// 实例化调用
var father = new Person("父亲", 40);
// 返回true,因为对象自身存在age属性
console.log("age" in father);
// 返回true,因为原型对象中存在该属性
console.log("sex" in father);
// 返回false,因为对象自身和原型对象中都不存在该属性
console.log("weight" in father);
回到前面的问题,如何判断一个属性是否存在于原型对象中?那么我们就可以使用in操作符,如果返回true就证明存在于对象自身或者原型中。那么我们再看是否在对象自身中,如果不存在,则一定存在于原型对象中。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
// 原型对象的属性和方法
Person.prototype.sex = "男";
Person.prototype.speak = function() {
console.log("我的名字叫:" + this.name);
};
Person.prototype.run = function() {
console.log("我今年" + this.age + "岁了");
};
}
}
// 实例化调用
var father = new Person("父亲", 40);
// 返回true,则证明存在于对象自身或者原型中
console.log("sex" in father);
// 返回false,说明存在于原型中
console.log(father.hasOwnProperty("sex"));
四、总结
构造函数、实例对象与原型对象的三角关系(重点):
- 构造函数的
prototype
属性指向构造函数的原型对象 - 实例对象是由构造函数创建的,那么实例对象的
__proto__
属相指向了构造函数的原型对象 - 构造函数的原型对象的
construcetor
属性指向了构造函数,实例对象的原型对象的constructor
属性也指向了构造函数
本片文章主要讲解的是ES5中的原型对象,并且与ES6中的结合,使用图文结合的方式完美的向大家介绍了ES5中的原型对象,希望大家能够牢记重点知识,砥砺前行,共同进步。同时也感谢优快云能够给大家这样一个互相学习借鉴的平台,祝愿优快云能够越来越好,吸引更多的学习者能够融入这个大家庭!