什么是原型(prototype)?
我们知道原生的js中并没有类(Class)这个概念, 因此在js中创建对象主要是通过以下两种方式实现:
js
//1.对象字面量
var xm = {name:"小明",age:18};
//2.构造函数
function Person(name,age){
this.name = name;
this.age = age;
}
let xm = new Person("小明",18);
其中前者适用于创建单个对象,当需要批量创建对象时,我们往往采取后者,即通过构造函数创建对象。
原型(prototype) 是函数的一个特定属性,该属性可看做一个指针,指向一个对象,该对象包含了构造函数的所有共有属性和函数。
简单来说原型相当于一个共享窗口,凡是放在原型里的属性都能共享给子代使用,但子代只有其使用权没有编辑和所有权,任何对象实例不能修改其父类的原型。
下面我们带入到一个简单的例子中具体感受一下。
js
function Person(name, age) {
//实例属性
this.name = name;
}
let xm = new Person("小明"); //实例一
let xh = new Person("小红"); //实例二
//原型函数
Person.prototype.greet = function(){
console.log("hello",this.name);
}
//原型属性
Person.prototype.age = 18;
xm.gender = "男"; //xm实例的自有属性
js
console.log(xm);
console.log(xh);
打印结果如下:
如图,xm实例有2个属性,xh实例只有1个属性。
其中多出的gender属性是xm特有的,只有xm实例有权访问, 这种只有实例本身可以访问的属性我们称为自有属性 。
相反的,属于同一个构造函数创建出来的对象属性则称为共有属性,共有属性包含原型属性和实例属性两部分。
实例属性 :在构造函数内部通过this定义的属性,每个实例对象都有其独立的实例属性。 例如:
this.name = name;
原型属性: 通过在构造函数的prototype上定义的属性,例如:Person.prototype.age = 18;
js
//Person的实例对象均可以使用定义在原型上的属性age
console.log(xm.age); //18
console.log(xh.age); //18
//Person的实例对象均可以使用定义在原型上的函数greet()
xm.greet(); //hello 小明
xh.greet(); //hello 小红
打印结果如下:
由上两次打印结果可知,添加在原型上的属性不属于任一实例,但是由该构造函数创建的实例均可以使用原型上的属性/函数
js
console.log("Person.prototype",Person.prototype);
打印结果如下:
如图,打印Person.prototype返回了一个对象,其内部包含了原型属性age,原型函数greet(),以及构造器创建的实例属性name,age。即该对象含了构造函数的所有共有属性和函数。
prototype、constuctor和__proto__的关系
prototype的定义已在上面有详细介绍,这里便不再赘述,主要介绍一下后两者的定义:
constructor
: 在 JavaScript 中, constructor 属性返回对象的构造函数。 说白了就是告诉你某个对象是通过哪个构造函数创建出来的。
__proto__
: 在 JavaScript 中,任何对象都有其隐式原型,对象的隐式原型指向创建该对象的父级的原型。
prototype和__proto__的区别:
- 任何对象都有__proto__,而prototype则是函数特有的属性
- 函数一定有原型,对象可能有原型,因为函数也是一个对象。
下面我们简单梳理一下构造函数,原型和隐式原型之间的关系:
- 实例的
__proto__
指向构造函数原型,告诉实例可以访问哪些共有属性 - 原型的
constructor
又指向构造函数本身,告诉实例是被哪个构造函数创建的 - 同时构造函数的
prototype
指向其原型,告诉其有哪些可被子实例访问的共有属性
通过prototype、constuctor和__proto__三个指针的共同作用,实现了js中的继承机制。
原型链
了解了prototype、constuctor和__proto__的关系,我们试着来看一张解释原型链的经典老图。
线条弯弯绕绕,一眼看过去可能还是摸不着头脑,不过没关系,我们把线条拉直了并分块来分析。
前面提到js中创建对象有两种方法:
- 通过构造函数创建
- 创建对象字面量
1. 构造函数创建的实例的原型链构成:
- 如红线所示,f1,f2是构造函数Foo()通过new关键字创造出的两个子实例;其隐式原型指向其构造函数Foo()的原型,原型的constructor又指向构造函数Foo()
- Foo()自己又是一个由Function()创建出来的子实例,同理可得其prototype、constuctor的指向。但Function已是最根处的构造函数,因此其
__proto__
无法继续向上指向,而是指向了自己 - 同时,不论是Function()还是Foo(),它们的prototype本质都是一个对象,因此其
__proto__
指向它们的构造函数Object()的原型 - 最终到达一切原型链的尽头Object.prototype.proto** ,其值为null**
2.通过对象字面量创建的实例的原型链构成:
- 如红线所示,o1,o2是通过对象字面量创建的两个子实例对象,因此其隐式原型指向它们的构造函数Object()的原型,原型的constructor又指向构造函数Object(),最终到达一切原型链的尽头Object.prototype.proto** ,其值为null**
- Object()自己又是一个由Function()创建出来的子实例,因此其
__proto__
指向Function()的原型
最后我们将上面两种情况合并到一起,便得到了完整的原型链构成:
原型链的作用
原型链本质上是js中用于实现继承的一套完整的机制。通过原型链,我们可以实现继承。开发者通过修改对象的原型,为该对象添加新的属性和方法,使多个对象可以共享同一个原型对象的属性和方法,从而简化代码量和优化内存空间。
new关键字的简易实现原理
了解了原型链的构成,我们通过new关键字的简易实现原理来更具体的体会原型的应用。
new关键字创建对象时实际上干了哪些事?
- 1.创建一个空对象 var obj={}
- 2.改变this指向 call apply
- 3.给该对象添加属性 this.name = name
- 4.返回一个对象 return obj
下面是一个简易的new关键字的实现:
js
function Person(name,age){
this.name = name
this.age = age
}
function _new(){
//1.创建一个空对象
var obj = {}
//2.取出参数
var params = Array.prototype.slice.call(arguments,1)
//3.取出构造函数
var foo = arguments[0]
//4.修改函数的this指向obj
foo.apply(obj,params)
//5.修改原型指向为function.prototype 否则根据原型链会直接指向最根处的Object
obj.__proto__ = foo.prototype
obj.__proto__.constructor = foo
//6.返回一个obj对象
return obj
}
const xm = new Person("小明",18);
const xiaoming = _new(Person,"小明",18);
console.log("new",xm);
console.log("_new",xiaoming);