对象
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)
暂无评论