这是我参与「掘金日新计划 · 4 月更文挑战」的第 1 天,点击查看活动详情
ES5的面向对象机制
js中的面向对象其实是基于原型的,构造函数其实充当了“类”的角色,因为构造函数可以创建实例。而构造函数其实与普通函数没有区别,只是一个普通函数用new进行调用的时候我们称它为构造函数。
如果在构造函数中显示return,若返回值是一个对象,则该对象会代替新创建的对象实例返回,但如果返回值是一个原始类型,则会被忽略。
function fn(name){
this.name = name;
return {b:'bb'}
}
let a = new fn('aa')
console.log(a); // {b: 'bb'}
function fn(name){
this.name = name;
}
let a = new fn('aa')
console.log(a); // fn {name: 'aa'}
应该始终确保使用new调用构造函数,否则构造函数中的this
指向的是全局对象。此时就是在改变全局对象,而不是创建一个新的对象。
function Person(name){
this.name = name;
}
let p = Person('flten');
console.log(p); // undefined
console.log(window.name); // flten
构造函数模式的问题:没有消除代码冗余,方法不共享
function Person(name){
this.name = name;
this.getName = function(){
console.log('name',name);
}
}
let p1 = new Person('aa');
let p2 = new Person('bb');
console.log(p1); // Person {name: 'aa', getName: ƒ}
console.log(p2); // Person {name: 'bb', getName: ƒ}
p1.getName = function(){
console.log('its change!')
};
p1.getName(); // its change!
p2.getName(); // name bb
查看/判断对象的原型对象
let o = {};
// 调用Object.getPrototypeOf()方法读取对象的[[Prototype]]属性值
// 直接声明的对象的[[Prototype]]属性值指向Object.prototype
Object.getPrototypeOf(o) === Object.prototype; // true
// 检查某个对象是否是另一个对象的原型对象
Object.prototype.isPrototypeOf(o); // true
// 也可以调用对象的__proto__方法查看对象的[[Prototype]]属性值
o.__proto__ === Object.prototype; // true
构造函数、原型对象和对象实例之间的关系:
对象实例和构造函数之间没有直接的关系,但是对象实例和原型对象,原型对象和构造函数之间都有直接联系。也就是对象实例和构造函数之间只有间接关系。如果切断对象实例和原型对象之间的联系,那么也就切断了对象实例和构造函数之间的联系。
(1)函数创建的时候会有同时创建一个prototype属性,指向一个对象,这个对象称为原型对象,也是构造函数实例共享属性/方法的载体。但这种联系是单向的,也就是函数通过prototype可以找到原型对象,但是原型对象如何指向该构造函数呢?
(2)原型对象身上有一个constructor属性指向它的原型对象,但这个属性是可以被修改的,为了保证指向不错乱,一般不去修改它。
(3)实例对象在通过构造函数new Fn()
创建时,需要建立它“父类”的关系,但是这个联系是在实例对象与构造函数的原型对象之间,因为共享(继承)的属性都在原型对象上。实例对象是通过一个内部属性[[prototype]]
指向它的原型对象的,这个内部属性我们访问不到,但是我们可以通过浏览器提供给我们的__proto__
属性去找到它的原型对象。到这里我们就建立了构造函数、原型对象、实例对象三者之间的联系。
(4)事实上所有的对象都有一个内部的[[prototype]]属性,即便是通过对象字面量声明的对象,例如:
let o = {name:'🍄'};
console.log(o.__proto__ === Object.prototype);
也就是说对象字面量方式声明的对象,它的原型对象是Object.prototype。既然说所有对象身上都有[[prototype]]属性,即它们也都有原型对象,那构造函数的原型对象自己本身是否有原型对象呢?答案是肯定的:
function fn(){};
fn.prototype.__proto__ === Object.prototype // true
即构造函数的原型对象的原型对象是Object.prototype。这里可能让你又疑惑,为什么不是fn.prototype.prototype
呢?首先prototype
是构造函数(其实是任何函数,因为任何函数都可以作为构造函数)身上的属性,对象身上时没有这个属性的,fn.prototype
原型对象是一个对象,对象身上没有prototype
这个属性,而作为对象它有一个内部的[[prototype]]属性,而这个属性可以通过浏览器提供的__proto__
属性可以访问得到。
既然fn.prototype.__proto__
的原型对象是Object.prototype
,那么Object.prototype
这个原型对象也一定有自己的原型对象了,那么是什么呢?
Object.prototype.__proto__ // null
结果是null
,多少有些出乎人的意料,但又是容易让人理解的。对象的原型总不能一直无限下去,自然会有一个终结的时候,而Object
作为顶级对象,它的原型指向null
颇有一番禅意,一路追溯下去发现起点是空。
(4)从上面我们可以看到,可以由fn
的原型对象fn.prototype
,通过它的__proto__
找到了它的原型对象Object.prototype
,又通过Object.prototype
的__proto__
找到了最后的null,这多么像一根链条⛓,可以一直向上查找,这就是原型链,对象继承其原型对象,而原型对象继承它的原型对象 ,依次类推。
function Person(name){
this.name = name;
}
let p = new Person('👨🏻');
console.log(p.__proto__.constructor.name); // Person
console.log(p.__proto__.__proto__.constructor.name); // Object
console.log(p.__proto__.__proto__.__proto__); // null
let person1 = {
name: '👩🏻',
sayName: function(){
console.log(this.name);
}
};
let person2 = Object.create(person1,{
name: {
configurable: true,
enumerable: true,
value: '👶🏻',
writable: true
}
});
person1.sayName(); // 👩🏻
person2.sayName(); // 👶🏻
对象继承
function Rectangle(length,width){
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function(){
return this.length * this.width;
}
function Square(size){
this.length = size;
this.width = size;
}
Square.prototype = new Rectangle();
Square.prototype.constructor = Square;
let square = new Square(12,13);
square.getArea(); // 144
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
console.log(square instanceof Object); // true
借用构造函数,实现组合继承。最主要的效率问题就是父类构造函数始终会被调用两次:一次在是创建子类原型时调用,另一次是在子类构造函数中调用
function Rectangle(length,width){
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function(){
return this.length * this.width;
}
function Square(size){
Rectangle.call(this,size,size);
}
Square.prototype = new Rectangle();
Square.prototype.constructor = Square;
let square1 = new Square(12); // 144
square1.getArea();
let square2 = new Square(13); // 169
square2.getArea();
寄生式组合继承
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype); // 创建原型对象
prototype.constructor = subType; // 修正constructor指向
subType.prototype = prototype; // 设置原型对象实现继承
}
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
// 借用父类构造函数
SuperType.call(this, name);
this.age = age;
}
// 设置原型,规避父类二次调用
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
};
let sub = new SubType('flten',25);
console.log(SubType.prototype.constructor.name); // SubType
console.log(sub.sayName()); // flten
console.log(sub.sayAge()); // 25
ES6的面向对象机制
类的数据类型本身就是函数
class Fn{}
typeof Fn; // 'function'
类的所有方法都定义在类的prototype属性上,在类的实例上调用方法,就是调用原型上的方法
class Person {
getName(){}
getAge(){}
}
let p = new person();
p.getAge === Person.prototype.getAge // true
类内部定义的方法都是不可枚举的,而构造函数prototype属性上定义的方法都是可枚举的
class Person {
getName(){}
getAge(){}
}
for(key in Person){console.log(key)}; // undefined
for(key in Person.prototype){console.log(key)}; // undefined
function PersonFn() {}
PersonFn.prototype.getName = function(){}
PersonFn.prototype.getAge = function(){}
for(key in PersonFn.prototype){console.log(key)}; // getName、getAge
类必须有constructor()方法,若无显式定义则会默认天剑,默认返回实例对象(this)。可以指定返回另一个对象,但这会导致继承上的问题,因此最好不要显式返回。
class Person {
getName(){}
getAge(){}
}
let p1 = new Person();
p1.constructor.name === Person; // true
class Person {
constructor(){
return {}
}
getName(){}
getAge(){}
}
let p2 = new Person();
p2.constructor.name === Person; // false
类必须使用new在对象实例化时调用,而构造函数可以作为函数直接调用
class Person {
getName(){}
getAge(){}
}
Person(); // Uncaught TypeError: Class constructor Person cannot be invoked without 'new'
function PersonFn() {}
PersonFn() // 可以正常调用
类方法内部this默认指向类实例,但单独使用该方法可能导致this指向错误,造成不可预料的问题。
class Person{
constructor(name,age){
this.name = name;
this.age = age;
}
getName(){
console.log(this.name)
}
}
let p1 = new Person('flten');
let {getName} = p1;
getName(); // Uncaught TypeError: Cannot read properties of undefined (reading 'name')
可以在构造函数中给方法绑定this
class Person{
constructor(name,age){
this.getName = this.getName.bind(this);
this.name = name;
this.age = age;
}
getName(){
console.log(this.name)
}
}
let p1 = new Person('flten');
let {getName} = p1;
getName(); // flten
类内部默认严格模式,不存在变量提升
静态方法不会被实例继承,只能通过类调用
class Person {
constructor(name,age){
this.name = name;
this.age = age;
}
static getAge(){
console.log('25');
}
getName(){
console.log(this.name);
}
}
class Flten extends Person {
constructor(name,age){
super(name,age)
}
}
const p1 = new Flten('flten',25);
p1.getName(); // flten
// 通过子类调用父类的静态方法
Flten.getAge(); // 25
// 使用实例对用静态方法,报错
p1.getAge(); // TypeError: p1.getAge is not a function
模拟抽象类
class staticClass {
constructor(){
if(new.target === staticClass){
throw new Error('staticClass是抽象类不能被实现')
}
}
}
let s1 = new staticClass(); // Error: staticClass是抽象类不能被实现
super super作为函数时: (1)子类构造器必须执行一次super函数 这是由于ES5和ES6继承机制的区别所决定的:ES5是新建子类的实例对象this,然后再将父类的属性添加到子类上;ES6是新建父类的实例对象this,再用子类构造函数修饰this,使得父类所有行为都可继承。 (2)super只能用在子类的构造函数中
class Person {
constructor(name,age){
this.name = name;
this.age = age;
}
static getAge(){
console.log('25');
}
getName(){
console.log(this.name);
}
}
class Flten extends Person {
constructor(){}
}
let p1 = new Flten('flten',25); // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
如果子类不显式声明constructor,则会在默认的constructor里执行super,因此不会报错
class Person {
constructor(name,age){
this.name = name;
this.age = age;
}
static getAge(){
console.log('25');
}
getName(){
console.log(this.name);
}
}
class Flten extends Person {}
let p1 = new Flten('flten',25);
p1.getName(); // flten
super作为对象时: (1)super在静态方法中指向父类,方法内部的this指向当前子类,而不是子类实例。这就相当于子类借用父类的静态方法。
class Person {
constructor(name,age){
this.name = name;
this.age = age;
}
static age = 25;
static getAge(){
console.log(this.age);
}
getName(){
console.log(this.name);
}
}
class Flten extends Person {
constructor(name,age){
super(name,age)
}
}
Flten.getAge(); // 25
(2)super在普通方法中指向父类原型对象,方法内部的this指向当前子类实例,定义在父类实例上的方法或属性无法通过super调用。
class Person {
constructor(name,age){
this.name = name;
this.age = age;
}
static getAge(){
console.log(this.age);
}
getName(){
console.log(this.name);
}
}
class Flten extends Person {
constructor(name,age){
super(name,age)
}
getAge(){
// 这里的super指向父类原型对象,但是super.getName方法内部的this是指向子类实例的
super.getName();
}
}
let p1 = new Flten('flten',25);
p1.getAge() // flten
(3)如果在普通方法中通过super对某个属性赋值,这时super就是this,赋值属性会变为子类实例属性,而读取super属性时,读取的是父类原型上的属性。
class A {
constructor() {
this.x = 1;
}
}
A.prototype.x = 5;
class B extends A {
constructor() {
super();
this.x = 2;
// 这里的super指向的是this,即实例对象
super.x = 3;
// 这里的super指向的是父类原型,读取的是父类原型上的属性
console.log(super.x); // 5
console.log(this.x); // 3
}
}
let b = new B();
(4)由于对象总是继承其他对象,因此可在任意对象中使用super
子类的继承机制:
子类作为对象,它的原型__proto__
指向父类,表示构造函数的继承
class A {}
class B extends A{}
B.__proto__ === A // true
子类作为构造函数,它的原型对象prototype
指向父类原型对象,表示方法的继承
class A {}
class B extends A{}
B.prototype.__proto__.constructor.name === 'A' // true