通过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'
我们可以通过instanceof
或new.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__
可以通过访问修改原型上的对象,使所有实例的原型对象修改;该操作比较危险,需谨慎使用!<u>若直接使用 [实例]. 的语法来修改对象,实际上是在实例本身进行对象操作(实例对象),则不会影响其他实例</u>(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 查看一下内容
如图: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 原型链常用方法这篇文章;本文也没有介绍继承相关的知识,限于篇幅,后期后单独写一篇文章进行介绍。
最后再强调一下,发现有问题和错误的请一定要指出来,大家一起交流学习
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。