1. 前言
说到原型,就必然涉及构造函数、类等概念,我会把这部分放到后边的拓展中,如果不清楚的可以先看拓展部分~
本文是找了一些资料(文章)看了后做的总结,有些地方可能理解的不是很深刻,有错误一定要帮我指出来,非常感谢~
最近在看《你不知道的Javascript》,后边看了原型的内容后会再更新==
2. 原型和原型链
正式开始前,大家记住这个:
- 对象:拥有
__proto__
和constructor
属性;__proto__
和constructor
对象独有的。
- 函数:拥有
prototype
、__proto__
和constructor
属性;prototype
是函数独有的;- 由于JS中函数也是对象,所以函数也有
__proto__
和constructor
属性。
- JS中引用类型都是对象,都有
prototype
、__proto__
和constructor
属性; - 注:
[[Prototype]]
和__proto__
其实是一回事:都表示原型链中的“连接”。在 JavaScript语言标准中用的[[prototype]]
(官方的),而__proto__
是很多浏览器提供的 (非官方);
2.1. 原型prototype
prototype
是函数独有的;- prototype就是一个对象,又叫做原型对象,可以通过
函数.prototype
访问; - prototype里通常有两个属性:
__proto__
和constructor
; - 原型可以用来共享方法;
- 原型中this的指向是实例。
2.2. 隐式原型__proto__
__proto__
是对象独有的;- 对象通过
__proto__
访问父构造函数的原型:对象.__proto__ === 父构造函数.prototype
。
2.3. 构造器constructor
constructor
是对象独有的;- 对象的构造函数指向父构造函数原型上的constructor属性:
对象.constructor === 父构造函数.prototype.constructor
。- 一般来说,构造函数原型的constructor指向构造函数本身,即:
对象.constructor === 父构造函数.prototype.constructor === 父构造函数
(特殊情况见constructor可能会丢失); - 注意:父构造函数原型里没有constructor属性时,会通过原型链找到再上一级构造函数原型的constructor(详见constructor可能会丢失)。
- 一般来说,构造函数原型的constructor指向构造函数本身,即:
注意:constructor可能会丢失
function Person(name) {
this.name = name;
}
Person.prototype = {};
const p = new Person('p');
p.name; // p
Person.prototype; // {} constructor属性丢失!
// 实例p的constructor不是Person!
p.constructor === Person; // false
// 往Person上边找
p.constructor === Person.prototype.__proto__.constructor // true
Person.prototype.__proto__.constructor === Object // true
p.constructor === Object // true
上例中,Person.prototype
赋值为空对象后,Person
自身没有constructor
属性了。此时通过原型链,Person.constrctor
指向了Person.prototype.__proto__.constructor
即Object
,从而实例p的constructor
属性也指向Object
。
如果不想丢失constructor,需要再加一句:
Person.prototype.constructor = Person;
2.4. 原型链
我们已经了解了原型prototype
和隐式原型__proto__
一个实例,可以通过__proto__
访问到父构造函数的原型;原型也可以通过__proto__
访问到原型的原型。调用一个实例的方法时,会从实例本身开始查找这个方法。如果实例本身没有,会通过__proto__
往父级查找,直到找到为止。如果找到终点也没找到,返回null
。
这个搜索的过程形成的链状关系就是原型链。
2.5. 小结
(1)对于对象:
对象拥有__proto__
和constructor
属性,没有prototype
:
对象.__proto__ === 父构造函数.prototype
;对象.constructor === 父构造函数.prototype.constructor
。一般来说,父构造函数.prototype.constructor === 父构造函数本身
;
(2)对于函数:
-
作为构造函数,独有
prototype
属性,构造函数.prototype
里有两个参数:constructor
和__proto__
:构造函数.prototype.constructor === 父构造函数.prototype.constructor
。一般来说,父构造函数.prototype.constructor === 父构造函数本身
;构造函数.prototype.__proto__ === 父构造函数.prototype
(比如Date.prototype.__proto__ === Object.prototype
)
-
函数也是对象,所以函数也拥有
__proto__
和constructor
属性,与(1)相同; -
注意:
构造函数.prototype.__proto__
和构造函数.__proto__
是不一样的。比如:Date.prototype.__proto__ === Object.prototype; //true // 因为原型的本质还是对象,即Date.prototype是一个对象; 且对象的父构造函数是Object。 Date.__proto__ === Function.prototype; // true // 因为Date本身是构造方法,它的父构造函数是Function。
3)作用域链顶层为null
。
3. 原型链图
我们来看一个简单的栗子:
function Father() {};
const son = new Father();
你知道son和Father的prototype
、__proto__
、constructor
是怎样吗?一起来画画吧~
第一步:prototype
这里有一个构造函数Father
,我们可以通过Father.protorype
访问到它的原型。
第二步:instance实例
- 实例是
new
出来的对象,所以实例拥有__proto__
和constructor
两个属性; - 实例对象没有
prototype
属性!(实例方法有==)
这里使用new操作符创建了一个实例对象son。
// 接上边栗子
// 实例对象没有prototype属性
son.prototype === undefined; // true
// 实例方法有prototype(这里的Father是Function的实例)
Father.prototype !== undefined; // true
Father.__proto__ === Function.prototype; // true
第三步:__proto__
实例的__proto__
属性指向父构造函数的prototype:Father.prototype === son.__proto__
第四步:constructor
第五步:Father的长辈们
往Father的父级找上去,最后son、Father、Function、Object之间的原型链图是这样的:
(建议先看黑色的线,看完再看看亮色的)
自测
我们画完了原型链图,想必下边这些判断对你来说已经so easy~
// 对象的__proto__指向父级的prototype
son.__proto__ === Father.prototype; // true
// 对象的构造器指向父构造函数
son.constructor === Father; // true
// 构造函数.prototype.constructor指向构造函数本身,所以实例对象的构造器也指向:构造函数.prototype.constructor
son.constructor === Father.prototype.constructor; // true
// 构造函数.prototype.constructor指向它本身
Father.prototype.constructor === Father; // true
// 构造函数.prototype是一个对象,对象的父级是Object,所以Father.prototype.__proto__指向Object.prototype
Father.prototype.__proto__ === Object.prototype; // true
// 构造函数Father的父级是Function,所以Father.__proto__指向Function.prototype
Father.__proto__ === Function.prototype; // true
// 构造函数的构造器是它的父级函数,所以...
Father.constructor === Function; // true
// 1) 构造函数.prototype.constructor指向构造函数本身,所以Function.prototype.constructor === Function; 2) 构造函数的构造器就是它的父级,所以Function.constructor === Function; 3) 所以两者相等
Father.constructor === Function.prototype.constructor; // true
// Function自己也是它的父构造函数的实例。这里是因为:构造函数.prototype.constructor指向构造函数本身
Function.prototype.constructor === Function; // true
// Function.prototype是一个对象,对象的上级是Object,所以...
Function.prototype.__proto__ === Object.prototype; // true
// Function自己也是它的父构造函数的实例,但它的父构造函数还是Function,所以...
Function.__proto__ === Function.prototype; // true
// Function.__proto__指向Function.prototype,是一个对象,对象没有prototype属性,所以是undefined
Function.__proto__.prototype === undefined; // true
// Function.__proto__指向Function.prototype, 构造函数.prototype.constructor指向自身,所以...
Function.__proto__.constructor === Function; // true
// Function.__proto__指向Function.prototype, 是一个对象,这个对象是Object的实例,所以...
Function.__proto__.__proto__ === Object.prototype; // true
// Function.__proto__.__proto__指向Object.prototype,构造函数.prototype.constructor指向构造函数自身,所以...
Function.__proto__.__proto__.constructor === Object; //true
// Function.__proto__.__proto__指向Object.prototype,Object.prototype的上一层是原型链的终点,JS规定值为null
Function.__proto__.__proto__.__proto__ === null; //true
// Object也是一个构造函数,构造函数.prototype.constructor指向构造函数自身
Object.prototype.constructor === Object; // true
// Object.prototype的上一层是原型链的终点,即null
Object.prototype.__proto__ === null; // true
// Object还是它父构造函数的一个实例,所以Object.__proto__指向了Function.prototype
Object.__proto__ === Function.prototype; // true
// Object.__proto__指向它的父构造函数.prototype,是一个对象,所以父构造函数.prototype再上一层则指向了Object.prototype
Object.__proto__.__proto__ === Object.prototype; // true
// Object.__proto__指向它的父构造函数.prototype,构造函数.prototype.constructor指向自身,所以...
Object.__proto__.constructor === Function; // true
// 1)Object.__proto__指向父构造函数.prototype,是一个对象;2)对象.__proto__指向Object.prototype;3)Object.prototype.__proto__为终点null
Object.__proto__.__proto__.__proto__ === null; // true
4. 拓展
4.1. 构造函数
- 从外观来看,构造函数跟普通函数其实没啥区别,但它可以用new关键字创建对象;
- 一般来说,公共属性定义到构造函数里面,公共方法我们放到原型对象身上(原型上的方法才会被实例共享)。
4.1.1. 实例成员和静态成员
- 实例成员:
- 在构造函数内部,通过this添加的成员;
- 只能通过实例化的对象来访问。
- 静态成员:
- 在构造函数本身上添加的成员;
- 只能通过构造函数来访问。
function Person(name, age) {
// 实例成员
this.name = name;
this.age = age;
}
// 静态成员
Person.sex = '女';
const p1 = new Person('LaoHuang', 24);
console.log(p1.name); // 'LaoHuang'
console.log(p1.age); // 24
console.log(p1.sex); // undefined (实例无法访问构造函数的静态成员)
console.log(Person.name); // Person undefined (构造函数无法直接访问实例成员,必须实例化后才能访问)
console.log(Person.sex); // 女 (构造函数可以访问它的静态成员)
4.1.2. new一个实例
这个过程就是实例化。
function Person(name) {
this.name = name;
}
const p1 = new Person('LaoHuang');
console.log(p1) // Person {name: "LaoHuang"}
p1就是一个实例对象。new一个新对象的过程,大概有以下几步:
- 创建一个空对象p1:
{}
; - 为p1准备原型链连接:
p1.__protp__ = Person.prototype
; - 绑定this,使构造函数的this指向新对象p1:
Person.call(this)
; - 为新对象的属性赋值:
p1.name
; - 返回this:
return this
。此时的新对象就拥有构造函数的方法和属性了。
4.1.3. 共享实例的方法
在构造函数的原型上添加的方法才会被实例共享。
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
console.log('名字:' + this.name);
}
const p1 = new Person('LaoHuang');
const p2 = new Person('FeiFei');
p1.getName(); // 名字:LaoHuang
p2.getName(); // 名字:FeiFei
console.log(p1.getName === p2.getName); // true
4.2. Class类
-
类的本质还是一个函数,它就是构造函数的另一种写法;
-
类没有变量提升,必须先定义,才能实例化;
-
constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,会默认添加一个空的constructor方法;
-
类的所有方法都定义在它的
prototype
上:class Person { constructor(name) { this.name = name; } getName() { console.log('名字:' + this.name); } } const p1 = new Person('LaoHuang'); const p2 = new Person('FeiFei'); p1.getName(); // 名字:LaoHuang p2.getName(); // 名字:FeiFei console.log(p1.getName === p2.getName); // true
4.2.1. 如何向类中添加方法
使用Object.assign()
。
class Person {
constructor(name, sex) {
this.name = name;
this.sex = sex;
}
getName() {
console.log('名字:' + this.name);
}
}
Object.assign(Person.prototype, {
getSex(){
console.log(this.name + '的性别是:' + this.sex)
}
});
const p1 = new Person('LaoHuang', '女');
const p2 = new Person('FeiFei', '男');
p1.getSex(); // LaoHuang的性别是:女
p2.getSex(); // FeiFei的性别是:男
console.log(p1.getSex === p2.getSex); // true
4.3. 类和构造函数的区别
- 类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行;
- 类的所有实例共享一个原型对象;
- 类的内部,默认就是严格模式,所以不需要使用
use strict
指定运行模式。
4.4. 继承
这里不展开讲继承,只涉及原型相关的。可以尝试着画一下原型图~
4.4.1. 构造函数+原型对象
function Person(name) {
this.name = name;
}
Person.prototype.getName = function () {
console.log('名字:' + this.name);
};
function Girl(name, sex) {
// 将实例化时,Person中的this指向当前实例
Person.call(this, name);
this.sex = sex;
}
// Girl.prototype = Person.prototype; // 用这种方式的话,给子类增加原型方法,同样会影响到父类
Girl.prototype = new Person();
Girl.prototype.sing = function () {
console.log('I am singing');
};
let g1 = new Girl('小红', '女');
console.log(Person.prototype); // {getName: ƒ, constructor: ƒ}
console.log(Girl.prototype); // Person {name: undefined, sing: ƒ}
它的原型链图是这样的:
从这张图可以看出:
// 1)g1.constructor指向Girl.prototype.constructor
// 2)而Girl.prototype是Person的实例,且Girl.prototype.constructor相当于(new Person).constructor,即Person.prototype.constructor,即Person本身
g1.constructor === Person; //true
思考一下:如果想让g1.constructor === Girl
,要怎么改呢?
4.4.2. extends语法糖
(1)super:
super
可作为函数和对象使用:
- 当作为函数使用时,只可在子类的构造函数中使用,表示父类的构造函数,但是
super
中的this
指向的是子类的实例,因此在子类中super()
表示的是Parent.prototype.constructor.call(this)
。 - 当作为对象使用时,
super
表示父类原型对象,即Parent.prototype
。
(2)extends干了啥:
- 第一步:继承父类的原型,将子类的
__proto__
指向父类本身; - 第二步:call继承,就是super()的处理过程。把父类的对象方法继承给子类对象;这也是为什么在es6的继承时必须要加上super(),因为不加的话无法继承到父类的对象属性。
- 第三步:创建子类自己的方法。
class Person {
constructor(name){
this.name = name;
}
getName(){
return this.name;
}
}
class Girl extends Person{
constructor(name, sex){
super(name);
this.sex = sex;
}
getSex(){
return this.getName() + '的性别:' + this.sex;
}
}
const g1 = new Girl('小红', '女');
console.log(g1.getSex()); // 小红的性别:女
它的原型链图是这样的(记得与第一种继承比较一下):
从这张图可以看出:
// 这是extends特殊的地方
Girl.__proto__ === Person; //true