boxmoe_header_banner_img

Hello! 欢迎来到我的博客!

加载中

文章导读

深入探究JS的对象,原型与原型链


avatar
xiaoifei 2025年3月16日 57

对象

JS中的对象是以键值对的形式出现的,创建一个对象十分简单

const p = {
    name: 'zhangsan',
    eat : function(){
        console.log("我吃饭了");
    }
}
console.log(p);

在其他面向对象的高级语言中,我们如果想要创建对象,就需要先使用,通过构造函数来创建实例化对象
但是在JS中,并不需要类就能创建对象,这是因为在JS最初的设计中由于并没有引入类,而是作为纯面向过程的函数式编程语言,来帮助人们更方便上手。
直到ES6,JS才引入类这一概念,但是它是基于原型设计的,而继承是通过原型链来实现

在JS中也有构造函数的概念,可以用来创建对象,我们可以理解为普通函数的特化,一个函数要想作为构造函数就需要使用new关键字

例如:

function Person(name){
};
const p = new Person();

这时就可以暂时将构造函数理解为类,通过构造函数,实现批量实例化对象
由此我们填补了拼图的第一块:从构造函数到对象

但是在JS中,通过实例化创建的对象其内部属性都会为其分配独立的空间

function Person(name){
    this.name = name;
    this.getName = function() {
        console.log(this.name);
    };
}
const p1 = new Person('1');
const p2 = new Person('2');
p1.getName();
p2.getName();
console.log(p1.getName === p2.getName);//false

比如在上面的成员方法中,两个实例化对象的同一个方法居然是不同的地址,也就是说出现了内存浪费,而原型就解决了这一问题
ps:在其他的编程语言(例如C++)中,类的成员方法都是存储在代码区中,每个对象都共享同一份函数代码,因此JS引入原型也更符合这一理念

原型

原型是什么?
我简单的理解为是一个以对象形式存在的模板。因此可以抛弃之前将构造函数作为类的想法,将原型看作是一个类,但是他是以对象存在的。
这里引用一句我认为特别恰当的话:

原型和原型链都是来源于对象而服务于对象的概念

构造函数中存在一个属性prototype,通过Person.prototype,我们可以访问到原型
至于一个函数为什么可以这么使用,这是因为在JS的世界,函数也可以是对象(雾)

function Person(name){
    this.name = name;
}
Person.prototype.getName = function() {
    console.log(this.name);
};
const p1 = new Person('1');
const p2 = new Person('2');
p1.getName();
p2.getName();
console.log(p1.getName === p2.getName);//true

可以发现不同对象共享原型上的方法,原型实现了属性和方法的共享

不仅仅构造函数能访问到原型,实例化对象也能访问到原型。当用 new 操作符调用构造函数(例如 new Person())时,生成的新对象的内部 [[Prototype]](可以通过 __proto__ 访问)会被设置为构造函数的 prototype 属性所引用的对象。(当然__proto__是只读的,因为它的值是由解释器自动赋值的)
由此我们填补了拼图的第二块:从构造函数和对象到原型

可以看到拼图少了最后一块,那就是原型上有一个属性constructor用来指回构造函数,这是十分关键的一步,因为原型就是在对象基础上多了constructor
由此我们填补了拼图的第三块:从原型到构造函数

我们知道原型就是一个对象,因此原型也能够重新赋值(手动修改构造函数的prototype指向),也就是说不需要解释器为我们创建一新的原型,我们自己就可以创建原型没然后绑定在构造函数的prototype上

function Person(){}

Person.prototype = {
	constructor: Person,//不能省略!!!
	sing: function(){
		console.log("唱歌");
	},
	dance: function(){
		console.log("跳舞");
	},
}
console.log(Person.prototype);
const p1 = new Person();
console.log(p1.__proto__);
console.log(Person.prototype===p1.__proto__);//true

但是要注意一点,就是要显式声明constructor指回构造函数,否则就不是一个原型,从而带来问题

对象属性/方法的搜寻过程

通过上面的描述我们大致了解了对象和原型的关系:构造函数创建出实例对象,对象带有一个proto指向构造函数自身的prototype;构造函数自身的prototype指向由解释器创造出一个原型(带constructor属性的对象),constructor指回构造函数

通过构造函数添加方法后访问Person.prototype

在原型上添加方法访问Person.prototype

如果一个对象在其本身和原型上有同名的方法,那么会优先执行谁呢?

function Person(name){
    this.name = name;
    this.getName = function() {
        console.log("对象上的方法:"+this.name);
    };
}
Person.prototype.getName = function() {
    console.log("原型上的方法:"+this.name);
};
const p1 = new Person('1');
p1.getName();
//对象上的方法:1

因此可以看到其搜寻顺序:对象上的属性->原型对象上的属性

原型继承

function Parent() {
  this.parentProp = "父类属性";
}
Parent.prototype.parentMethod = function() {
  console.log("父类方法");
};

function Child() {
  this.childProp = "子类属性";
}

p1 = new Parent();//创建了一个父亲的实例化对象
console.log(p1);//此时还不是原型,因为没有constructor属性

Child.prototype = p1;// 关键:子类原型指向父类实例化对象
Child.prototype.constructor = Child;// 修复 constructor 指向 : 此时父亲的实例化对象正式变成了原型,因为指定了consturctor

const c = new Child();
console.log(c);
console.log(c.parentProp); // "父类属性"
c.parentMethod(); // "父类方法"

缺点

  • 父类实例属性会被所有子类实例共享(引用类型属性可能被意外修改)。
  • 无法向父类构造函数传参。

原型链

在了解完原型继承后,我们会发现当输出Person.prototype的时候会有一个[[prototype]]属性(通过__proto__)访问
这个属性常出现在实例化对象上用来访问自己的原型(比如Person构造创建的p的proto就指向Person.prototype这个原型)

也就是说,原型就是对象,还能指向自己的原型

一般来说这个原型是编译器帮我们创建的(当使用Person构造的时候,就自动创建了一个带有constructure和proto=Object的对象绑定到Person.prototype)
默认创建出的原型的原型其实是指向Object.prototype。

function Parent() {
}
const p = new Parent();  
console.log(Parent.prototype.__proto__ === Object.prototype);//true

因此搜寻顺序进一步拓展:对象上的属性->原型对象上的属性->原型的原型对象上的属性->…->Object的原型对象上的属性->null

使用instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上

console.log(p instanceof Object);//true


评论(0)

查看评论列表

暂无评论


发表评论

表情 颜文字
插入代码