创建对象的几种基本方式
在说原型之前,我们先来看看创建对象的几种基本方式:
1. 对象字面量
var person = {
name:"Jack",
age:15
say:function(){
alert(this.name+"今年"+this.age);
}
};
2. 使用 Object 构造
var person = new Object();
person.name = "Jack";
person.age = 15;
person.say = function() {
alert(this.name+"今年"+this.age);
}
以上两种方式是最基本的创建方式,但是在创建多个同类型对象的时候就显得很笨拙,会产生大量重复代码。
需要创建多个同类型对象的时候可以使用下面的方法 ——
3. 构造函数的方式
function Person(name,age) {
this.name = name;
this.age = age;
this.say = function() {
alert(this.name+"今年"+this.age);
}
}
var my = new Person1("Jack",15);
var my = new Person2("Lily",13);
但是这种方式也不够优秀,因为this在对象实例化的时候发生改变,新实例的方法也要重新创建。也就是每次实例化都要重新创建新的方法。我们可以更优雅一些 —— 让实例化对象们共享属性和方法,共有的方法只创建一次。这时候就需要用到原型
4. 构造函数+原型
function Person(name,age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function() {
alert(this.name+"今年"+this.age);
}
var my = new Person1("Jack",15);
var my = new Person2("Lily",13);
把公共的属性和方法放在原型中,实例就可以实现“共享”,从而达到只需创建一次的目的。
下面我们再来仔细看看,原型相关的——
构造函数、原型、实例三者之间的关系
- 每一个构造函数都拥有一个
prototype
属性,这个属性指向一个对象,也就是原型对象 - 原型对象默认拥有一个
constructor
属性,指向指向它的那个构造函数 - 通过构造函数
new
就可以生成一个实例对象 - 每个实例对象都拥有一个隐藏的属性
__proto__
,指向它的原型对象
function Person() {
}
var p = new Person()
Person.prototype === p.__proto__ //true
Person.prototype.constructor === Person //true
使用构造函数创建的实例对象的特点
- 实例可以共享原型上的属性和方法
- 实例自身的属性(自有属性)会覆盖原型上面的同名属性,实例自身没有的属性会去原型找
function Person(name,age){
this.name = name
this.age = age
}
Person.prototype.say = function() {
console.log("Hi, I'm " + this.name + ","+ this.age + "years old.")
}
var person1 = new Person("Lily",15);
var person2 = new Person("Jack",16);
原型链
- 所有原型链的终点都是 Object 函数的
prototype
属性 Objec.prototype
指向的原型对象同样拥有原型,不过它的原型是null
,而null
则没有原型
修改原型
原型实际上也是个对象,当然也是可以修改的。我们可以直接在原型上增加属性和方法:
function Person(name,age){
this.name = name
this.age = age
}
Person.prototype.say = function() {
console.log("Hi, I'm " + this.name + ","+ this.age + "years old.")
}
var person1 = new Person("Lily",15);
var person2 = new Person("Jack",16);
Person.prototype.country = "China"
Person.prototype.intro = function() {
console.log("Hi, I'm " + this.name + ","+ this.age + "years old.")
}
person1.country //China
person1.intro() //Hi, I'm Lily,15years old.
但是 在修改原型的时候需要注意:尽量避免在已创建实例之后重写原型。
在已创建实例的情况下重写原型,会切断已创建实例跟原型的关系,造成‘原型链混乱’。
我们可以看看,正常情况下的原型关系是这样的:
function Person(name,age){
this.name = name
this.age = age
}
Person.prototype.say = function() {
console.log("Hi, I'm " + this.name + ","+ this.age + "years old.")
}
var person1 = new Person("Lily",15);
var person2 = new Person("Jack",16);
Person.prototype === person1.__proto__ //true
Person.prototype.constructor === Person //true
person1.__proto__.constructor === Person //true
而重写原型对象之后,我们可以看到,新原型对象仍是构造函数的原型,但已经不是实例对象的原型;且新原型对象的构造函数并不指向Person,而是指向Object;但构造函数又还是实例的原型的构造函数…… 场面十分混乱。我们还是看图:
//- 基于上面的代码重写原型
Person.prototype = {
name:"Hello",
age:18,
say:function(){
console.log("Hi!")}
}
Person.prototype === person1.__proto__ //false
Person.prototype.constructor === Person //false
person1.__proto__.constructor === Person //true
Person.prototype.constructor === Object //true
如果非要重写原型,那就 在重写原型对象的时候指定 constructor
。但是这种方式也并非完美,即便构造函数与原型的关系被保留了,但已创建的实例对象与原型的关系仍然是切断的。此外,以这种方式重设 constructor
属性会导致它的 Enumerable
特性被设置成 true
(默认为 false
)
关于ES6中的 class
ES6中的class,实际上就是构造函数的另一种写法(语法糖)。 本质上都是通过原型、原型链去定义方法、实现共享
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
// 可以这么改写
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
参考链接:
http://www.php.cn/js-tutorial-417938.html
https://xxxgitone.github.io/2017/06/10/JavaScript%E5%88%9B%E5%BB%BA%E5%AF%B9%E8%B1%A1%E7%9A%84%E4%B8%83%E7%A7%8D%E6%96%B9%E5%BC%8F/