JavaScript中,构造函数、原型对象以及实例对象是构建面向对象程序设计的核心组成部分。它们各自扮演着不同的角色,并且通过特定的方式相互作用,共同支撑起JavaScript独特的面向对象模型。本文将深入探讨这三者之间的关系及其重要性。
在讨论之前,先了解下如何创建一个对象。
创建对象
在JS中,创建一个对象最直接的方式是对象字面量。它简单、直接、随意,但不够灵活。
let cao = {
name: '小超',
}
let fan = {
name: '范总',
age: 17,
}
从这代码中很明显发现,我们直接用let
创建了cao
和fan
两个对象,非常简单直接。
在ES5的时候,是通过构造函数和new
关键字来创建对象。该方法是基于JS的原型继承机制。
function Person(name, age) {
console.log(this);
this.name = name;
this.age = age;
}
const dys = new Person('昌总', 19) // 以构造函数方式运行 产生对象
const dyf = new Person('啊威', 20)
在这,则是用构造函数和new
来创建对象。
但在ES6出现以后,引入了class
关键字,使得可以批量创建对象,更加直观和易于理解。虽然该方法很像Java或c语言中创建对象,但本质上还是基于原型的继承。
// class es6
class Person {
constructor(name,age) {
this.name = name;
this.age = age;
}
eat() {
console.log('吃饭');
}
}
let wei = new Person('啊威',18);
let guo = new Person('过帅',18);
很明显,因为ES6的存在,创建对象相对之前更简单方便。
从上文中不难发现,在ES6出现之前,一直是通过构造函数和 new
关键字来创建对象。既然如此,那就让我们来探究下构造函数
构造函数
在JS中,创建对象的任务是由函数承担起来的,为此就诞生了构造函数。构造函数是JavaScript中用于创建和初始化对象的一种特殊类型的函数。构造函数的首字母要大写,用来与普通函数进行区分。
function Person(name, age) {
this.name = name;
this.age = age;
}
判断函数是否是构造函数,是根据new
关键字决定的。只有用new
来创建对象,该函数才会是构造函数。
请看如何用new
关键字创建对象:
- 用new关键字创建一个空对象。
- 将这个新对象的内部
[[Prototype]]
(即__proto__
)链接到构造函数的prototype
属性上。 - 将构造函数内的
this
绑定到新创建的对象上。 - 执行构造函数中的代码,初始化对象的属性或方法。
- 返回新创建的对象.
const wen = new Person('文', 18);
console.log(wen.name, wen.age);
const chen = new Person('陈', 18);
console.log(chen.name, chen.age);
这样我们就创建了一个对象。但我们可以发现,该对象没有任何方法,如果要调用方法的话,这就涉及另一个知识点:原型。
原型(Prototype)
原型能使得JavaScript能够实现基于原型的继承。每个函数都有一个prototype
属性,指向一个对象,这个对象就是该函数作为构造函数时所创建实例的原型。通过原型链,JavaScript实现了对象间的属性和方法共享。
因为有原型的存在,我们可以直接将方法定义在构造函数的prototype上,让所有通过该构造函数创建的实例共享同一个方法。
const cy = {
name: '',
palyBasketball: function() {
console.log('我在打篮球');
}
}
function Person(name,age) {
console.log(this);
this.name = name;
this.age = age;
}
// 原型 cy
Person.prototype = cy;
const wu = new Person('',19);
wu.palyBasketball();
console.log(wu.__proto__ === cy);
原型的运行机制则依赖于原型链。当尝试访问一个对象的属性时,如果该对象本身没有这个属性,JavaScript引擎会沿着原型链向上查找,直到找到该属性或到达原型链的顶端。
// 定义父类
function Animal(name) {
this.name = name;
}
// 在Animal的原型上定义方法
Animal.prototype.speak = function() {
console.log(this.name + ' makes a noise.');
};
// 定义子类
function Dog(name, breed) {
Animal.call(this, name); // 调用父类构造函数
this.breed = breed;
}
// 设置Dog的原型为Animal的一个实例
Dog.prototype = Object.create(Animal.prototype);
// 修正constructor属性
Dog.prototype.constructor = Dog;
// 在Dog的原型上定义方法
Dog.prototype.bark = function() {
console.log(this.name + ' barks.');
};
// 创建Dog的实例
var dog = new Dog('Rex', 'German Shepherd');
// 属性和方法的访问
dog.speak(); // 输出: Rex makes a noise.
dog.bark(); // 输出: Rex barks.
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
// 查看原型链
console.log(dog.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
通过这个例子,展示了原型链是如何进行继承和查找的,让我们来仔细分析下。
1.原型链的构建:
Dog.prototype
被设置为Animal.prototype
的一个实例,这意味着所有Dog
实例都可以访问Animal.prototype
上的方法。
2.属性查找:
当调用dog.speak()
时,首先会在dog
中查找方法,如果没有则沿着
dog.__proto__
(即Dog.prototype
)查找,如果还找不到,则依旧往上层寻找,直到找得到为止。
3.扩展:
如果在Animal.prototype
上添加一个新方法,所有Animal
及其子类的实例都会立即拥有这个新方法。
在这简单提下,原型和原型对象的区别。
- 原型是一个概念,描述了JavaScript中的继承机制,它涉及如何通过原型链查找属性和方法。
- 原型对象是具体的对象,它是函数的
prototype
属性所指向的对象,用于存储可以被构造函数生成的所有实例共享的方法和属性。
实例对象
实例对象是通过构造函数或者是类创建的具体对象。每个实例对象都拥有自己的属性和方法,并可以通过原型链去访问。创建实例对象在上文都有提及,就不过多赘述。现在来讲下实例对象的特性。
1.独立属性:每个实例对象都有自己的属性。这些属性可以是构造函数中直接定义的,也可以是通过其他方式动态添加的。
2.共享方法:实例对象可以通过原型链访问其构造函数的prototype
上定义的方法,可以通过this
指针来找到对象。这些方法在所有实例之间共享,节省了资源且提高了性能。
3.原型链:每个实例对象都有一个内部[[Prototype]]
属性(通常可以通过__proto__
访问),指向其构造函数的prototype
。当尝试访问一个实例对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript引擎会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的顶端。
三者关系
构造函数、原型对象和实例对象三者关系可以概括如下:
- 构造函数通过
new
关键字创建实例对象,把this
作为指针,指向实例对象。 - 实例对象通过
_proto_
来指向原型对象。 - 原型对象通过构造函数中
prototype
属性进行访问和修改。
正是这三者相互关联,才形成了JS面向对象的原型继承机制。
总结
理解这些重要概念不仅能让我们了解JS面向对象的底层机制,还有助于提高我们的代码编写能力。希望本文能在你走向JS的道路上有所帮助。