es5 es6+ 类相关知识总结

538 阅读8分钟

通过es5和es6对类的操作对比来系统的了解 类,原型,原型链等知识;相对es6的class写法,es5更容易体现出原型的作用

一、类

1、类的声明&实例化

es5:

// 声明
function Person() {}
// 实例化
var person = new Person();

es6:

// 声明
class Person {}
// 实例化
const person = new Person();

ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。


2、实例方法、实例属性、原型方法、原型属性、静态方法、静态属性

es5:

function Person() { // 构造函数
    // 实例属性
    this.name = '张三';
    // 实例方法(本质也是实例属性,getName为函数表达式,赋值给this.getName)
    this.getName = function() {              
        return this.name;
    };
}
// 静态静态属性
Person.age = 1;    
// 静态方法
Person.getAge = function() {      
    return this.age;
}
// 原型属性
Person.prototype.sex = '男';    
// 原型方法
Person.prototype.getSex = function() { 
    return this.sex;
}

var person1 = new Person();
var person2 = new Person();

es6:

class Person { // 类,本质也是构造函数
    constructor {
        this.name = '张三'; // 实例属性
        this.getName = function() {  // `实例方法`  严格来说也是实例属性,将方法赋值给属性getName   
            return this.name;
        };
    }
    // name = '张三'; // 实例属性 等价于上面在constructor中的写法
    // getName = function() { // 实例方法
    //     return this.name;
    // }
    static age = 1; // 静态属性(tips: 该写法之前只是提案,但笔者在chrome浏览器测试可用)
    static getAge() { // 静态方法
        return this.age; // 注意静态方法的this指向的是类本身,而非实例
    }
    getSex() { // 原型方法
        return this.sex;
    }
}

es5 与 es5 对比

构造函数的prototype属性,在ES6的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面(静态方法除外)。

class P {
    fn() {
        ...
    }
}
// 上面的方法等同于
P.prototype = {
    function fn() {
        ...
    }
}

由于类的方法都定义在prototype对象上面,所以类的新方法可以添加在prototype对象上面。Object.assign方法可以很方便地一次向类添加多个方法。

class P {
  constructor(){ // constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。
    // ...
  }
}

Object.assign(Point.prototype, {
  fn1(){},
  fn2(){}
});

prototype对象的constructor属性,直接指向“类”的本身,这与ES5的行为是一致的。

P.prototype.constructor === P

另外,类的内部定义的所有方法,都是不可枚举的(non-enumerable)。这一点与ES5的行为不一致。

function P() {
    ...
}
P.prototype.fn = function(){}
Object.keys(P.prototype) // ["fn"]
Object.getOwnPropertyNames(P.prototype) //  ["constructor","toString"]

class P {
  constructor(x, y) {
    // ...
  }
  fn() {
    // ...
  }
}

Object.keys(P.prototype) // []
Object.getOwnPropertyNames(P.prototype) //  ["constructor","fn"]

类的属性名,可以采用表达式。

const fnName = "fn";
class P {
  constructor() {
    // ...
  }

  [fnName]() {
    // ...
  }
}

Object.getOwnPropertyNames(P.prototype) // ["constructor", "fn"]
2.1 实例对象

实例对象,即实例本身可以访问的对象;无法通过构造函数访问; 实例方法和实例属性会挂载到原型本身;实例方法不会被其他实例共享;

检查属性的方法有两种:
Object.prototype.hasOwnProperty:检查对象自有属性是否包含指定属性 [对象.hasOwnPropertu(属性名)]
in 关键字:检查原型链上是否包含指定属性 [属性名 in 对象]

person1.hasOwnProperty('name'); // true
person1.hasOwnProperty('getName)'; // true
person1.name;  // 张三 - 实例可以直接访问实例属性
person1.name = '张四'; 
person2.name;  // 张三 - 修改实例属性不会影响其他实例

es5的构造函数和es6的类 都需要通过new关键字来进行实例化,但调用class若不加new则会报错,而调用构造函数即使不加new也不会报错,这一点es5和es6表现不一致;

es5:
function P(){}
var p = P() // underfind
es6:
class P {}
const p = P() // Uncaught TypeError: Class constructor P1 cannot be invoked without 'new'

我们可以通过instanceofnew.target来保证没有new关键字,同样可以创建类的实例(非class方式)

es5:
// instanceof
function P() {
    if(!this instanceof P) {
        return new P(arguments);
    }
}

// new.target
function P() {
    if(new.target !== P) {
        return new P(arguments);
    }
}
2.2 原型对象

原型对象,即构造方法原型prototype上的对象;实例可直接访问(通过原型链查找__proto__);构造方法可通过原型prototype进行访问; 原型方法和原型属性会挂载到构造函数的原型上,并且被所有实例所共享 tips:实例通过隐式原型__proto__可以通过访问修改原型上的对象,使所有实例的原型对象修改;该操作比较危险,需谨慎使用!若直接使用 [实例]. 的语法来修改对象,实际上是在实例本身进行对象操作(实例对象),则不会影响其他实例(tips: 此处指 实例.属性名 = ***实例.__proto__.属性名 = ***的区别)

Person.prototype // {sex: "男", getSex: ƒ, constructor: ƒ}
Person.prototype.hasOwnProperty('sex') // true
Person.prototype.hasOwnProperty('getSex') // true
Person.prototype.sex; // 男 构造方法可通过原型`prototype`进行访问原型属性
person1.__proto__ === person2.__proto__ // true
person1.sex; // 男
person2.sex; // 男
person1.__proto__.sex = '女';
person2.sex; // 女;
person1.getSex = function() {
   return '男';
}
person1.getSex(); // 男
person2.getSex(); // 女 这里的表现和上面不一样,因为 person1.getSex = ... 的方式只是为person1实例添加了一个实例方法,并未作用到原型链上,故不会共享到person2上
// 我们可以打印一下person1 查看一下内容

658078d1585454b626b37092a2dd7b3d.png 如图:person1实例上新增了一个getName方法,该方法就是我们通过person1.getSex添加的,而其原型链__proto__ 上面的getSex则为实例所共享的原型方法;访问的时候,会先在实例本身查查找要访问的属性,若不存在,则会在其原型__proto__上继续查找,直至__proto__为null,这就是原型链;关于原型链本文后续会详细介绍;

2.3 静态对象

静态对象,即构造函数本身添加的对象;构造函数可直接访问,实例无法访问;

person1.age;        // undefind - 类的实例无法获取静态属性
person1.getAge;   // undefind - 类的实例无法获取静态方法
Person.age; // 1 - 静态属性只能通过构造函数本身访问
Person.getAge; // 1 - 静态方法只能通过构造函数本身访问

es6中,静态方法可以通过static关键字声明,但通过static声明属性目前只是提案

为什么使用静态方法:阻止方法被实例继承,类的内部相当于实例的原型,所有在类中直接定义的方法相当于在原型上定义方法,都会被类的实例继承,但是使用static静态方法定义的不会被实例继承,而且可以被实例直接应用

静态方法可以被子类继承,也可以被super调用 

class P {
    constructor(){ ... };
    static fn () { console.log('parent') };
}
class C extends P {
    constructor(props) {
        super(props);
    }
    static getParentStaticFn () {
        
    }
}
const p = new P();
p.fn(); // TypeError: (intermediate value).fn is not a function
P.fn(); //  parent
C.getParentStaticFn(); // parent

静态方法的this指向类本身


二、原型链

通过上一节的介绍,我们对类及类相关的属性方法有了大概的了解,本节则是系统的归纳一下各个方法及属性的关系,及其所构成的原型链

1、prototype 原型

原型指的就是一个对象,实例“继承”那个对象的属性。在原型上定义的属性,通过“继承”,实例也拥有了这个属性。“继承”这个行为是在 new 操作符内部实现的

原型与构造函数的关系就是,构造函数内部有一个名为 prototype 的属性,通过这个属性就能访问到原型:

class Person {}

Person.prototype
// {
//     constructor: class Person
//     __proto__: Object
// }

2、实例

使用 new 操作符实现实例化,并通过 instanceof 来检查他们之间的关系:

const person = new Person();

person instanceof Person; // true
person.constructor === Person; // true

我们在类的原型上添加属性,则其实例则会‘继承’该属性:

Person.prototype.type = 'class';

person.type; // class

hasOwnProperty() 方法用来检测一个属性是否是对象的自有属性,而不是从原型链继承的。如果该属性是自有属性,那么返回 true,否则返回 false。换句话说,hasOwnProperty() 方法不会检测对象的原型链,只会检测当前对象本身,只有当前对象本身存在该属性时才返回 true。

3、__proto__ 隐式原型

实例通过 __proto__ 来访问原型

Person.prototype === person.__proto__

4、constructor 构造函数

构造函数通过 prototype 访问原型; 原型通过 construcor 访问构造函数;

Person.prototype.constructor === Person; // true

5、实例、构造函数、原型之间的关系

Person // 构造函数
person // 实例
person = new Person
Person.prototype // 原型
person.__proto__ // 隐式原型

Person.prototype === person.__proto__
Person.prototype.constructor === Person
person.__proto__.constructor === Person

在读取一个实例的属性的过程中,如果属性在该实例中没有找到,那么就会循着 __proto__ 指定的原型上去寻找,如果还找不到,则尝试寻找原型的原型:

class P {
    constructor() {
        this.name;
    }
    setName(value) { // ES6 方法被挂在到prototype上而非类本身
        this.name = value;
    }
    getName = () => { // 箭头函数则挂在到对象本身
        return this.name;
    }
}

const p = new P();

6、原型链

结语

本文主要为了简单整理一下构造函数的相关知识,只做学习交流。文中难免出现错误,若发现问题还请及时指出,感谢!

文中提到了 hasOwnProperty() in 等方法和操作符,这些属于对象相关的知识点,本文并未做详细解释,后期会更新相关文章,感兴趣可以先看一下JavaScript 原型链常用方法这篇文章;本文也没有介绍继承相关的知识,限于篇幅,后期后单独写一篇文章进行介绍。

最后再强调一下,发现有问题和错误的请一定要指出来,大家一起交流学习

参考资料:

《图解原型和原型链》 JS菌

《ES6 class》