浅谈原型
@jarringslee
构造函数
构造函数本质上仍是普通函数,只是在使用 new
关键字时,会充当“构造函数”的角色。通过 new
调用,函数会开启对象实例化流程(创建对象、绑定 this
、执行代码、返回对象)——这正是 “构造函数” 名字的来由。
function Person(name, age) {
this.name = name;
this.age = age;
this.species = '人类';
this.say = function () {
console.log("泥嚎");
}
}
let per1 = new Person('ljl', 19);
let per2 = new Person('cjx', 19);
构造函数通常首字母大写,这不仅是命名习惯,也提醒我们它与普通函数的不同:它用于生成对象实例。
如果把所有实例都在构造函数内部定义它们的属性和方法,每创建一个实例就重复一次,这样既浪费内存,也不利于共享公共逻辑。为了解决这个问题,引入了原型机制:
- 构造函数有一个
.prototype
属性,指向“原型对象”; - 将共享的方法或属性放在原型对象上,所有实例都可以访问,从而实现 代码复用 和 内存优化。
原型对象
刚才提到了,只有函数才有prototype属性,即在js中,每一个函数类型的数据,都有一个叫做prototype的属性,这个属性指向的是一个对象,就是所谓的原型对象。
对于原型对象来说,它有个constructor属性,指向它的构造函数。
那么这个原型对象有什么用呢?最主要的作用就是用来存放实例对象的公有属性和公有方法。
例如刚才的Person中:
Person.prototype
是一个属性(对象),默认存在于所有函数上。- 它不会存储实例属性(如
name
,age
,species
这些实例属性时存储在每个实例对象本身的,而原型对象主要用于存储方法和共享的属性),而是用来存放构造函数所有实例共享的方法和属性,避免每个实例重复创建相同的方法,从而节省内存。
举个例子,如果改写成:
Person.prototype.say = function () {
console.log("泥嚎");
};
Person.prototype.species = '人类';
那么所有通过 new Person()
创建的实例就会共享 say()
和 species
,而不是每个实例里都单独创建一遍。这样更高效、更符合原型继承模型。
console.log(per1.species); // 人类
console.log(per2.species); // 人类
per1.say(); // 泥嚎
per2.say(); // 泥嚎
console.log(per1.constructor); // Person()
我们通过查找原型的方法将原型分为两种:
- 显示原型
显示原型就是利用prototype属性查找原型,只是这个是函数类型数据的属性。 - 隐式原型
隐式原型是利用__proto__
属性查找原型,这个属性指向当前对象的构造函数的原型对象,这个属性是对象类型数据的属性,所以可以在实例对象上面使用:
console.log(per1.__proto__ === Person.prototype); // true
console.log(per2.__proto__ === Person.prototype); // true
根据上面,就可以得出constructor、prototype和__proto__之间的关系了:
原型链
每当我们使用 new 关键字创建一个对象时,这个对象会自动获得一个内部属性 [[Prototype]],指向构造函数的原型对象(即 Person.prototype)。
当我们访问对象的属性或方法时,JS引擎首先会在对象本身查找。如果找不到,它会沿着 [[Prototype]] 链向上查找,直到找到该属性或方法,或者到达原型链的末端 null 为止。
function Person(name) {
this.name = name;
}
Person.prototype.species = '人类';
const per1 = new Person('ljl');
console.log(per1.species); // 输出 '人类'
在这个例子中:per1 是通过 new Person(‘ljl’) 创建的对象。
per1 的 [[Prototype]] 指向 Person.prototype。
当我们访问 per1.species 时,JavaScript 引擎首先在 per1 本身查找 species 属性,未找到。
然后,它沿着原型链向上查找,最终在 Person.prototype 上找到了 species 属性,值为 ‘人类’。
这种机制使得对象能够继承构造函数原型对象上的属性和方法,实现了代码的复用和内存的优化。
什么是_proto_
?
在 JavaScript 中,每个对象都有一个内部属性 [[Prototype]]
,通常通过 __proto__
访问。当我们访问对象的属性时,JavaScript 引擎首先在对象本身查找,如果找不到,则沿着 [[Prototype]]
链向上查找,直到找到该属性或达到原型链的末端。
构造函数的原型对象(如 Person.prototype
)也是对象,因此它也有自己的 [[Prototype]]
,指向 Object.prototype
。这意味着,Person.prototype
继承自 Object.prototype
,形成了原型链的一部分。然而,Object.prototype
的 [[Prototype]]
是 null
,即原型链的终点。
假设我们有以下代码:
function Person(name) {
this.name = name;
}
Person.prototype.species = '人类';
const per1 = new Person('ljl');
console.log(per1.species); // 输出 '人类'
在这个例子中:
per1
的[[Prototype]]
指向Person.prototype
。Person.prototype
的[[Prototype]]
指向Object.prototype
。Object.prototype
的[[Prototype]]
是null
,形成了原型链的终点。
当我们访问 per1.species
时,JavaScript 引擎首先在 per1
本身查找,未找到;然后在 Person.prototype
查找,找到并返回 '人类'
。
JavaScript 的原型链是由多个对象通过 [[Prototype]]
连接而成的链条。每个对象都可以通过 __proto__
访问其原型对象,直到达到 Object.prototype
,其 [[Prototype]]
为 null
,标志着原型链的终点。
函数对象
在 JavaScript 中,函数不仅仅是可以调用的代码块,它本身也是一种对象。这意味着函数除了可以执行,还可以拥有属性和方法。
函数对象的属性:
每个函数对象都有一些内置属性:
length
:表示函数形参的个数。name
:表示函数的名称。prototype
:函数对象特有的属性,指向该函数的原型对象(之前我们讲过)。- 其他通过
__proto__
可以访问到的属性,来源于函数继承的对象。
例如:
function foo(a, b) {
return a + b;
}
console.log(foo.length); // 2
console.log(foo.name); // "foo"
console.log(foo.__proto__ === Function.prototype); // true
这里我们可以看到,函数的隐式原型 __proto__
指向 Function.prototype
,而 Function.prototype
本身是一个函数对象,也继承自 Object.prototype
。
函数既可以调用也可以作为对象使用.因为函数是对象,所以可以给函数添加自定义属性或方法:
function greet() {
console.log("Hello!");
}
greet.version = '1.0';
console.log(greet.version); // 输出 "1.0"
这说明函数不仅能执行,还可以像普通对象一样存储数据。
构造函数本身也是函数对象.前面我们提到的构造函数,例如 Person
,也是函数对象。它可以被 new
调用生成实例,同时它也可以拥有自己的属性和方法:
function Person(name) {
this.name = name;
}
Person.species = '人类';
console.log(Person.species); // 输出 "人类"
这里的 Person.species
就是直接添加在函数对象上的属性,而不是实例对象上的属性。每个实例访问不到 Person.species
,因为它是函数对象自身的属性。
函数对象也有自己的原型链:
myFunction --> Function.prototype --> Object.prototype --> null
- 每个函数对象的
__proto__
指向Function.prototype
。 Function.prototype
也是一个函数,它的__proto__
指向Object.prototype
。Object.prototype
的__proto__
为null
,标志着原型链终点。
这也是为什么函数既能调用自身方法,又能继承对象的通用方法的原因。
总结
- 构造函数是使用 new 关键字的函数,用来创建对象,所有函数都是 Function() 的实例
- 原型对象是用来存放实例对象的公有属性和公有方法的一个公共对象,所有原型对象都是 Object() 的实例
- 原型链又叫隐式原型链,是由 proto 属性串联起来,原型链的尽头是 Object.prototype