JavaScript是一门面向对象的语言,所有的对象都从原型继承属性和方法,那么什么是原型?对象与对象之间如何实现继承?
本文就带大家来深入理解下JavaScript中的原型,欢迎各位感兴趣的开发者阅读本文。
接下来我们来逐步分析下原型与对象之间的关系。
我们使用function
关键字来创建函数时,内存中会创建一个包含prototype
属性的对象,这个属性指向函数的原型对象,如下所示:
function Person() {
}
Person.prototype // {constructor: Person(), __proto__}
上述代码中我们创建了一个名为Person的函数:
constructor
与__proto__
属性。image-20210310173710555
我们画个图来描述下Person
与prototype
之间的关系
image-20210310195733848
用new运算符调用的函数便为构造函数,建议构造函数命名时将首字母大写。
上个章节我们捋清了构造函数与原型对象的关系,接下我们来看下函数实例与原型对象之间的关系。
我们用运算符new
将上个章节创建的Person
函数进行实例化,得到person实例,代码如下:
// 实例化对象
const person = new Persion();
在上个章节中,我们知道原型对象有2个属性,其中__proto__
是每一个除null外的JavaScript对象都具有的一个属性,它指向该对象的原型对象。
接下来,我们来证明下person.__proto__
是否和Persion.prototype
相等,代码如下:
function Person() {
}
const person = new Persion();
console.log("函数实例的__proto__指针指向构造函数的原型对象: ", person.__proto__ === Person.prototype);
执行结果如下:
image-20210312172907318
除了使用
__proto__
来访问原型对象,我们还可以使用Object.getPrototypeOf()来获取。
证明出他们相等后,结合构造函数与原型对象可知他们三个之间的关系,如下所示:
image-20210310202939966
当我们实例化一个构造函数时,也会为这个实例创建一个
__proto__
属性,这个属性是一个指针,它指向构造函数的原型对象。 由于同一个构造函数创建的所有实例对象的__proto__
属性都是指向其构造函数的原型对象,因此所有的实例对象都会共享构造函数原型对象上的属性和方法,因此,一旦原型对象上的属性或方法发生改变,所有的实例对象都会受到影响。
上个章节我们分析了原型对象中__proto__
的指向,接下来我们来分析下constructor
的指向。每个原型对象都有一个constructor
属性,它指向该对象的构造函数。
接下来,我们来证明下Person.prototype.constructor
是否和Person
相等,代码如下:
function Person() {
}
const person = new Person();
console.log("原型对象与构造函数相等: ", Person.prototype.constructor === Person);
执行结果如下:
image-20210310211445102
证明出他们相等后,我们结合构造函数、函数实例、原型对象可知他们四个之间的关系,如下所示:
image-20210310212117231
获取对象原型,除了访问它的prototype外,我们还可以使用Object.getPrototypeOf()来获取。
读取实例中的属性时,如果找不到,就会查找该对象原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
接下来,我们举个例子来证实下上述话语:
function Person() {
}
Person.prototype.name = "原型上的name属性";
const person = new Person();
person.name = "实例上的name属性";
console.log(person.name) // 实例上的name属性
delete person.name;
console.log(person.name); // 原型上的name属性
delete Person.prototype.name;
console.log(person.name); // undefined
我们来分析下上述例子:
在上面的分析中,我们在Person的原型的原型中没找到name属性,那么Person的原型的原型是什么呢?我们在谷歌浏览器的控制台来打印下看看,如下所示:
image-20210310222010988
正如上图结果所示,Person的原型的原型是一个对象,证明出了原型也是一个对象,那么我们就可以用最原始的方式创建它,代码如下所示:
const object = new Object();
object.name = "对象中的属性";
console.log(object.name); // 对象中的属性
console.log("object实例与Object的原型对象相等", object.__proto__ === Object.prototype);
console.log("Object的原型对象与构造函数相等", Object.prototype.constructor === Object);
执行结果如下:
image-20210312175303383
知道原型也是对象后,结合我们上面所证明出来的内容,他们之间的关系如下所示:
image-20210310224603680
通过前面的分析我们知道了万物基于Object,那么Object的原型是什么呢?答案是null
我们在谷歌浏览器的控制台上验证下,结果如下所示:
image-20210310231037617
综合上述,他们最终的关系如下所示:
image-20210310232450892
图中橙色线条组成的链状结构就是原型链。
我们在实现实现一些功能时,经常会用一个包含所有属性和方法的对象字面量来重写整个原型对象。
如下所示:
Person.prototype = {
name: "神奇的程序员",
age: "20",
job: "web前端开发",
sayName: function () {
console.log(this.name);
}
}
由于重写的对象中不存在constructor属性,那么它的constructor属性将会指向Object。
我们来验证下,代码如下所示:
console.log("Person的原型对象的构造函数与Person构造函数相等", Person.prototype.constructor === Person)
执行结果如下:
image-20210312211047400
如果constructor的值很重要,那么我们就需要特意将constructor的指向改为构造函数了,代码如下所示:
Person.prototype = {
name: "神奇的程序员",
age: "20",
job: "web前端开发",
sayName: function () {
console.log(this.name);
},
constructor: Person
}
console.log("Person的原型对象与Person构造函数相等", Person.prototype.constructor === Person)
执行结果如下:
image-20210311000952986
前面的原理分析章节中,在最后的示意图中,我们很直观的看到了原型链的样子,接下来我们来捋一下原型链的具体概念。
constructor
)__proto__
)接下来,我们通过一个例子来讲解下原型链的继承,代码如下:
function Super() {
this.property = true;
}
Super.prototype.getSuperValue = function() {
return this.property;
}
function Sub() {
this.subProperty = false;
}
// Sub原型指向Super实例,constructor被重写,指向Super
Sub.prototype = new Super();
Sub.prototype.getSubValue = function () {
return this.subProperty;
}
let sub = new Sub();
console.log("获取Super的属性值", sub.getSuperValue());
console.log("sub实例的原型对象等于Sub构造函数的原型对象", sub.__proto__ === Sub.prototype);
console.log("Sub构造函数的原型对象的原型对象等于Super构造函数的原型对象", Sub.prototype.__proto__ === Super.prototype)
console.log("Sub构造函数的原型对象constructor指向Super的构造函数", Sub.prototype.constructor === Super)
运行结果如下:
image-20210312215707154
getSuperValue
方法,返回函数内部的property
属性getSubValue
方法,返回函数内部的subProperty
属性接下来,我们将上述分析内容画成图,更好理解一点,如下所示(橙色线条为原型链):
image-20210311112200899
我们使用原型链来实现继承时,如果继承了原型对象上的引用类型,那么这个引用类型将会被所有实例共享。
接下来我们通过一个例子来说明下这个问题:
function Super() {
this.list = ["a","b","c"];
}
function Sub() {
}
Sub.prototype = new Super();
const sub1 = new Sub();
sub1.list.push("d");
console.log(sub1.list);
const sub2 = new Sub();
console.log(sub2.list);
上述代码中:
sub1
实例[ 'a', 'b', 'c', 'd' ]
sub2
实例[ 'a', 'b', 'c', 'd' ]
运行结果如下:
image-20210311141113229
问题很明显了,我们没有向sub2的list数组中添加元素,我们希望它的值是Super原型上定义的["a","b","c"]
。
由于Super构造函数中定义的list属性是引用类型,因此在实例化时它被共享了,sub1实例改了它的值后,sub2实例化时拿到的就是sub1实例改后的值,即[ 'a', 'b', 'c', 'd' ]
在下个章节中,我们将解决引用类型被实例共享的问题
我们在子类的构造函数中,可以使用call
将父类构造函数的所有属性和方法拷贝到当前构造函数中,这样我们在实例化后,修改属性和方法就是修改的复制过来的内容了,不会影响父类构造函数中的内容。
接下来,我们通过一个例子来讲解下上述话语:
function Super() {
this.list = ["a","b","c"];
}
function Sub() {
Super.call(this)
}
const sub1 = new Sub();
sub1.list.push("d");
console.log("sub1" ,sub1.list);
const sub2 = new Sub();
console.log("sub2", sub2.list);
上述代码,我们沿用的上个章节的的例子,这里只讲改变的部分:
运行结果如下:
image-20210312112758516
我们借用构造函数实现继承,无法继承父类原型上的方法和属性,那么函数的复用性也就就没了。
我们继续拿上个章节的代码来举例:
function Super() {
this.list = ["a","b","c"];
}
Super.prototype.newList = [];
function Sub() {
Super.call(this)
}
const sub1 = new Sub();
console.log("sub1" ,sub1.newList);
newList
属性运行结果如下:
image-20210311160013667
经过前两个章节的分析,我们知道了原型链继承方式可以继承原型对象上的属性和方法,构造函数继承可以继承构造函数中的属性和方法,他们彼此互补,那么我们将它俩的长处结合起来,就实现了组合继承,也完美的弥补了它们各自的短板。
接下来,我们通过一个例子来讲解下组合继承:
// 组合继承
function Super(name) {
this.name = name;
this.list = ["a","b","c"];
}
Super.prototype.getName = function () {
return this.name;
}
function Sub(name, age) {
// 构造函数继承,第二次调用父类构造函数
Super.call(this,name);
this.age = age;
}
// 原型链继承,第一次调用父类构造函数
Sub.prototype = new Super();
Sub.prototype.constructor = Sub;
Sub.prototype.getAge = function () {
return this.age;
}
const sub1 = new Sub("神奇的程序员","20");
sub1.list.push("d");
console.log("sub1", sub1.getName());
console.log("sub1", sub1.getAge());
console.log("sub1", sub1.list);
const sub2 = new Sub("大白","20");
console.log("sub2", sub2.getName());
console.log("sub2", sub2.getAge());
console.log("sub2", sub2.list);
上述代码中:
Super
的函数,接受一个name参数,构造函数中有两个属性name和listSuper
的原型对象上添加getName
方法,返回Super中的name属性值Sub
的函数,接受两个参数:name、age,在构造函数中添加age属性,继承父类构造函数中的属性与方法Sub
的原型对象上添加getAge
方法,返回Sub中的age属性运行结果如下:
image-20210311175629506
我们在实现组合继承时,调用了两次父类的构造函数。
第一次调用父类构造函数时:
Sub.prototype
这一次调用就是我们在原型链继承章节所讲的内容,子类已经继承了父类构造函数中的属性与方法和原型对象上的属性与方法。
第二次调用父类构造函数时:
call
会将Super
中的属性和方法赋值给Sub的实例上因此,第二次调用时,我们就没有必要将构造函数中的属性和方法赋值给Sub构造函数的实例了,这是无意义的。
接下来,我们来看下优化后的组合继承:
function Super(name) {
this.name = name;
this.list = ["a", "b", "c"];
}
Super.prototype.getName = function () {
return this.name;
}
function Sub(name, age){
Super.call(this, name);
this.age = age;
}
// 创建一个中间函数,用于继承Super的原型对象
function F() {
}
// 将F的原型对象指向Super的原型对象
F.prototype = Super.prototype;
// 将Sub的原型对象指向F的实例
Sub.prototype = new F();
Sub.prototype.constructor = Sub;
Sub.prototype.getAge = function () {
return this.age;
}
const sub1 = new Sub("神奇的程序员","20");
sub1.list.push("d");
console.log("sub1", sub1.getName());
console.log("sub1", sub1.getAge());
console.log("sub1", sub1.list);
const sub2 = new Sub("大白","20");
console.log("sub2", sub2.getName());
console.log("sub2", sub2.getAge());
console.log("sub2", sub2.list);
上述代码中:
F
Super
的原型对象他的高效之处在于只有在实例化时才会调用一次Super构造函数,并且保持原型链的不变。
运行结果如下:
image-20210311212111885
优化后的组合继承又名寄生式组合继承,在上面的实现代码中,我们使用一个中间函数实现原型链的继承,这个中间函数也可以可以使用
Object.create()
来替代,他们的实现原理都一样。 那么,在重写Sub构造函数的原型对象时,我们就可以这样写:Sub.prototype = Object.create(Super.prototype, {constructor: {value: Sub}})
上个章节中,我们使用一个中间函数实现了原型链的继承。我们还可以直接将子类的原型对象通过__proto__
属性将其指向父类的原型对象,这种方式没有改变子类的原型对象,所以子类原型对象上的constructor
属性还是指向父类的构造函数。
接下来,我们通过一个例子来讲解下上述话语:
function Super(name) {
this.name = name;
this.list = ["a", "b", "c"];
}
Super.prototype.getName = function () {
return this.name;
}
function Sub(name, age){
Super.call(this, name);
this.age = age;
}
// 修改Sub构造函数的原型对象指向改为Super的原型对象
Sub.prototype.__proto__ = Super.prototype;
Sub.prototype.getAge = function () {
return this.age;
}
const sub1 = new Sub("神奇的程序员","20");
sub1.list.push("d");
console.log("sub1", sub1.getName());
console.log("sub1", sub1.getAge());
console.log("sub1", sub1.list);
const sub2 = new Sub("大白","20");
console.log("sub2", sub2.getName());
console.log("sub2", sub2.getAge());
console.log("sub2", sub2.list);
上述代码中:
__proto__
属性修改了Sub原型对象指向在原理解析章节中,我们知道每一个除null
外的javascript对象都有一个__proto__
属性,默认指向这个对象的原型对象,因此我们可以通过这个属性来修改它指向的原型对象。
我们还可以使用ES6中的
Object.setPrototypeOf()
方法来修改对象的原型。 那么,上述例子中的代码就可以改为:Object.setPrototypeOf(Sub.prototype, Super.prototype)
直接向构造函数上添加一个方法,这个方法就是静态方法。
我们前面讲的所有的继承方法,都没有实现构造函数上的静态方法继承,然而在ES6的class
继承中,子类是可以继承父类的静态方法的。
我们可以通过Object.setPrototypeOf()
方法实现静态方法的继承。
接下来,我们通过一个具体的例子来讲解下:
function Super(name) {
this.name = name;
this.list = ["a", "b", "c"];
}
Super.prototype.getName = function () {
return this.name;
}
// 添加静态方法
Super.staticFn = function () {
return "Super的静态方法";
}
function Sub(name, age) {
// 继承Super构造函数中的数据
Super.call(this, name);
this.age = age;
}
// 修改Sub的原型指向
Object.setPrototypeOf(Sub.prototype, Super.prototype);
// 继承父类的静态属性与方法
Object.setPrototypeOf(Sub, Super);
Sub.prototype.getAge = function () {
return this.age;
}
console.log(Sub.staticFn());
const sub1 = new Sub("神奇的程序员", "20");
sub1.list.push("d");
console.log("sub1", sub1.list);
console.log("sub1", sub1.getName());
console.log("sub1", sub1.getAge());
const sub2 = new Sub("大白", "20");
console.log("sub2", sub2.list);
console.log("sub2", sub2.getName());
console.log("sub2", sub2.getAge());
运行结果如下:
image-20210312000444311
上述代码中的其他部分与之前举的例子相同,我们来分析下不同之处:
Super
构造函数添加了一个名为staticFn
的静态方法Object.setPrototypeOf(Sub, Super)
函数继承了Super
构造函数的静态属性与方法至此,我们就实现了一个完美的继承,也正是ES6的class语法糖的底层实现。
ES6中新增了一个class
修饰符,我们用class
修饰符创建两个对象后,我们就可以使用extends
关键词来实现继承了。它的底层实现就是我们上面所讲的寄生式组合继承结合构造函数的静态方法继承来实现的。
接下来,我们来看下上个章节中举的例子,如何使用class实现,代码如下:
class Super {
constructor(name) {
this.name = name;
this.list = ["a","b","c"];
}
getName() {
return this.name;
}
}
// 向Super添加静态方法
Super.staticFn = function () {
return "Super的静态方法";
}
class Sub extends Super{
constructor(name, age) {
super(name);
this.age = age;
}
getAge() {
return this.age;
}
}
console.log(Sub.staticFn())
const sub1 = new Sub("神奇的程序员", "20");
sub1.list.push("d");
console.log("sub1", sub1.list);
console.log("sub1", sub1.getName());
console.log("sub1", sub1.getAge());
const sub2 = new Sub("大白", "20");
console.log("sub2", sub2.list);
console.log("sub2", sub2.getName());
console.log("sub2", sub2.getAge());
运行结果如下:
image-20210312004416434
本系列文章的所有示例代码,请移步:js-learning
本文为《JS原理学习》系列的第2篇文章,本系列的完整路线请移步:JS原理学习 (1) 》学习路线规划
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有