什么是原型对象
在 JS 中仅函数拥有一个prototype
属性,这个属性指向一个对象,称之为原型对象,原型对象又拥有一个constructor
属性,指向它的函数.
如何创建原型对象
// 创建了一个构造函数
function Girl(name, age) {
this.name = name;
this.age = age;
}
// 创建了一个对象
const girlFriend = new Girl("无", "-1");
可以看到在DevTools中显示该对象上有一个[[Prototype]]
属性,点开[[Prototype]]
显示的就是girlFriend.__proto__
的属性,指向的是girlFriend
的构造函数(Girl
)的原型对象
!. 在谷歌浏览器的 DevTools 中显示的[[ProtoType]]
就是指__proto__
!. 火狐浏览器 DevTools 中显示的是<prototype>
,同样指的是__proto__
!. 主流浏览器环境中的__proto__
是 ES 标准中[[Prototype]]
的实现,在其他 ES 实现环境中可能就不存在__proto__
这个属性,如Node
我们接着往下看,点开 girlFriend 对象的[[Prototype]]
(girlFriend.__proto__
)后又有一个[[Prototype]]
属性,这个属性指向的是Object
构造函数的原型对象
原因很简单,Girl
的原型对象是一个Object
实例,它是由Object
构造函数创建的,所以它指向Object
构造函数的原型对象
可以看到这个原型对象上没有了[[Prototype]]
属性,那么这个原型对象的原型对象就是null
!. 上面那张图片上有一个__proto__
,但它并不是我们前面说的在浏览器环境中实现的__proto__
属性,它是一个getter
函数的返回值,它和原型和原型链没有关系,它的作用涉及到浏览器内部原型链的构建,我们作为开发人员不需要关注__proto__
这个属性,后面会详细介绍
阅读了上面,我们从一个对象出发,访问到一个原型对象,又从这个原型对象上访问到另一个原型对象,是不是就像构成了一条链子,我们称之为原型链
原型链
上面的例子会形成一个原型图,在这个原型图中有三条原型链:
prototype
这个属性只存在于函数上__proto__
指向该对象的构造函数的原型对象Function
构造函数是由自己构造的,所以其__proto__
指向Function
的原型对象Object
是一个函数,它的构造函数是Function
,所以指向Function
的原型对象- 原型对象是一个
Obejct
对象,除Object
的原型对象外,都指向Obejct
的原型对象 Objcet
的原型对象的__proto__
指向NULL
,是原型链的终点
继承(原型的作用)
原型链继承
// 创建一个构造函数
function Girl(name, age) {
this.name = name;
this.age = age;
}
// 创建一个构造函数
function myGirl(name) {
this.name = name;
}
// 将myGirl构造函数的原型指向一个Girl实例
myGirl.prototype = new Girl("武则天", 18);
const undefinedGirl = new myGirl("无");
console.log(undefinedGirl.name); // '无'
console.log(undefinedGirl.age); // 18
通过上述代码我们可以发现,在myGirl
上我并没有定义age
属性,但却可以访问到,是因为:
当我们访问对象的属性或方法时,JavaScript 引擎会沿着原型链查找,直到找到相应的属性或方法或者达到原型链的末端(null)
我们没有在myGirl
上定义age
属性,我们通过设置myGirl
的prototype
(原型对象),实现了在myGirl
上可以访问到Girl
上的属性,即实现了继承
缺点
因为继承的原型对象为同一个,若其中一个继承了该原型对象的实例对象修改了原型上的属性,其他实例对象均会受到影响
构造函数继承
// 定义一个函数
function Girl(name) {
this.name = name;
}
// 设置该构造函数的原型
Girl.prototype.sayHello = function () {
console.log(`I am ${this.name}`);
};
// 定义一个构造函数
function myGirl(name, love) {
// 调用Girl方法并将其this指向为由该构造函数创建的实例对象
Girl.call(this, name);
// 子类特有的属性
this.love = love;
}
// 创建对象
const girlFriend = new myGirl("myself", true);
girlFriend.sayHello(); // Uncaught TypeError: girlFriend.sayHello is not a function
缺点
- 这种继承方式只能继承父类中的属性和方法,无法继承父类原型中的属性和方法
- 无法实现复用
组合继承
组合继承就是上面俩种方式的结合,用原型链实现对父类原型属性和方法的继承,用构造函数继承的思想来实现父类的属性和方法的继承
// 定义一个函数
function Girl(name) {
this.name = name;
}
// 设置该构造函数的原型
Girl.prototype.sayHello = function () {
console.log(`I am ${this.name}`);
};
// 定义一个构造函数
function myGirl(name, love) {
// 调用Girl方法并将其this指向为由该构造函数创建的实例对象
Girl.call(this, name);
// 子类特有的属性
this.love = love;
}
// 将子类的原型指向父类的实例对象
myGirl.prototype = new Girl();
// 设置constructor属性,指向子类构造函数
myGirl.prototype.constructor = myGirl;
// 创建对象
const girlFriend = new myGirl("丁真", true);
const mistress = new myGirl("雪豹", false);
girlFriend.sayHello(); // I am 丁真
mistress.sayHello(); // I am 雪豹
缺点
子类的原型上会包含父类的实例,导致原型链上存在多余的属性和方法
原型式继承
通过一个已有的对象创建一个新的对象,新对象的原型链上包含了原始对象的引用
function createGirlFriend(obj) {
function F() {}
F.prototype = obj;
return new F();
// 可直接用Object.create(obj, ...options);
}
const girl = {
name: "蔡徐坤",
say: function () {
console.log(`my name is ${this.name}`);
},
};
const myGirlFriend = createGirlFriend(girl);
myGirlFriend.name = "007";
myGirlFriend.say(); // my name is 007
缺点
原型式继承不支持像构造函数继承那样传递参数给父对象的构造函数。
寄生式继承
和原型式继承几乎没有区别
function createGirlFriend(obj) {
function F() {}
F.prototype = obj;
// 新对象继承原对象的属性,并在新对象上添加额外的方法
F.method = function () {
// .....
};
return new F();
// 可直接用Object.create(obj, ...options);
// ....
// ....
}
区别在于寄生式继承额外在新对象上添加了一些方法或属性,而原型式继承则更侧重于复制对象的结构