如果你点进了这篇文章,说明(大概率)你和我一样,是一个对class知之甚少的前端菜鸟。本文是我根据自己的学习笔记整理而得,我会在文章中尽量用我们菜鸟听的懂的方式来说清楚class和function的异同,与此同时,会列出相关的知识来源,供大家分析判断我的讲解是否有误,并辅助大家更好的理解相关内容,如果你嫌文字太多,不看知识来源部分也完全ok。好了,不说废话,开冲!
一些前置知识点
对象的构造函数
知识来源
constructor: A class or function that specifies the type of the object instance
The
constructor
property is writable, non-enumerable, and configurable.
我们生成实例时, 用于说明该实例类型的 class 或 function ,就是构造函数。比如
function Person(name) {
this.name = name
}
const aZhen = new Person('阿珍')
const aQiang = new Person('阿强')
console.log(aZhen.constructor === Person) // true
阿珍和阿强就是实例,用于指定阿珍、阿强类型的Person函数, 就是构造函数。
对象的 [[Prototype]]
知识来源
JavaScript 中所有的对象都有一个内置属性,称为它的 prototype(原型)。它本身是一个对象,故原型对象也会有它自己的原型,逐渐构成了原型链。原型链终止于拥有
null
作为其原型的对象上。备注: 指向对象原型的属性并不是
prototype
。它的名字不是标准的,但实际上所有浏览器都使用__proto__
。访问对象原型的标准方法是Object.getPrototypeOf()
。
每个对象实例“天生”就有一个内置的原型对象([[Prototype]]),但是一定要注意,[[Prototype]] 并非存储在prototype
字段上,现有的浏览器都 [[Prototype]] 通过__proto__
字段暴露出来,但是最最最官方的方法,是通过Object.getPrototypeOf(objInstance)
访问 [[Prototype]]。(你可以理解为,__proto__
是浏览器厂商们自己商量,创造出来的第三方属性,而非js的原生属性)
function Person(name) {
this.name = name
}
const aZhen = new Person('阿珍')
const aQiang = new Person('阿强')
console.log(aZhen.prototype) // undefined (实例中没有这个字段)
console.log(Object.getPrototypeOf(aZhen) === aZhen.__proto__) // true
说到 [[Prototype]],就不得不提大名鼎鼎的原型链,顾名思义,多个相关的 [[Prototype]] 构成了原型链:
当访问一个对象实例的某个属性时,首先会先在这个对象实例本身上找,如果没有找到,则会去他的 [[Prototype]] 上找,还没有找到,就再去 [[Prototype]] 的 [[Prototype]] 上去找,这样一层一层向上查找,直到找到值为null
的 [[Prototype]] 为止。
构造函数的prototype
知识来源
The
prototype
data property of aFunction
instance is used when the function is used as a constructor with thenew
operator. It will become the new object's prototype.
A function's
prototype
property, by default, is a plain object with one property:constructor
, which is a reference to the function itself. Theconstructor
property is writable, non-enumerable, and configurable.
每个构造函数“天生”也有个内置的prototype
属性。对象实例的 [[Prototype]],指向的就是生成该实例的构造函数的prototype
属性。
console.log(Object.getPrototypeOf(aZhen) === Person.prototype); // true
函数的prototype
默认是一个只有constructor
属性的对象,constructor
指向该构造函数本身,且不可枚举。
console.log(Person.prototype.constructor === Person) // true
知识来源
If the
prototype
of a function is reassigned with something other than anObject
, when the function is called withnew
, the returned object's prototype would beObject.prototype
instead. (In other words,new
ignores theprototype
property and constructs a plain object.)
若我们对函数的prototype
重新赋值,情况将会发生变化:
-
赋值后的
prototype
不是Object
此时构造函数所生成对象实例的 [[Prototype]] 将不再指向构造函数的
prototype
,而是直接指向Object.prototype
Person.prototype = 'string' // aZhen是在prototype重新赋值后生成的。 // 若aZhen的生成发生在prototype重新赋值之前,又是另一种情况了,怕说太多怕增加文章的理解难度,本文就不再讨论了 const aZhen = new Person('阿珍') console.log(Object.getPrototypeOf(aZhen) === Object.prototype) // true
-
赋值后的
prototype
是Object
此时构造函数所生成对象实例的 [[Prototype]] 仍然指向构造函数的
prototype
,但构造函数的prototype.constructor
将不再指向构造函数本身。我们可以通过以下方法,手动对prototype.constructor
重新赋值。Person.prototype = { type: 'person' } console.log(Object.getPrototypeOf(aZhen) === Person.prototype); // true console.log(Person.prototype.constructor) // function Object() { [native code] } // 手动对constructor重新赋值 Object.defineProperty(Person.prototype, "constructor", { enumerable: false, value: Person })
new
运算符
知识来源
The
new
operator lets developers create an instance of a user-defined object type or of one of the built-in object types that has a constructor function
When a function is called with the
new
keyword, the function will be used as a constructor.new
will do the following things:1. Creates a blank, plain JavaScript object. For convenience, let's call it
newInstance
.2. Points
newInstance
's [[Prototype]] to the constructor function'sprototype
property, if theprototype
is anObject
. Otherwise,newInstance
stays as a plain object withObject.prototype
as its [[Prototype]].3. Executes the constructor function with the given arguments, binding
newInstance
as thethis
context (i.e. all references tothis
in the constructor function now refer tonewInstance
).4. If the constructor function returns a non-primitive, this return value becomes the result of the whole
new
expression. Otherwise, if the constructor function doesn't return anything or returns a primitive,newInstance
is returned instead. (Normally constructors don't return a value, but they can choose to do so to override the normal object creation process.)
new
运算符用于创建指定类型的对象实例,当使用new
运算符创建对象实例时,会依次发生以下事件:
- 生成一个空对象O
- 如果构造函数的
prototype
是Object
,O的[[Prototype]]
将指向构造函数的prototype
, 如果构造函数的prototype
不是对象, O的[[Prototype]]
将指向Object.prototype
- 执行构造函数,并且执行过程中构造函数内的
this
指向O - 若构造函数返回值为引用类型, 则将其作为本次
new
表达式的返回值,若构造函数返回值为简单类型,或者构造函数没有返回值,生成的对象O将作为new
表达式的返回值
class和funciton的对比
知识来源
Classes in JS are built on prototypes but also have some syntax and semantics that are unique to classes.
Classes are in fact "special functions"
class其实就是function和原型链的语法糖,你可以理解为开发者们觉得通过function和 [[Prototype]] 来定义类型太麻烦且可读性差,因此把这些他们觉着麻烦的东西封装起来,包装成用起来更方便、更易于理解的class。
下面我带大家看一下,要如何通过class来改写基于function的旧代码,从而帮助大家更好的理解class。
类型定义及创建实例
function
function Person(name, gender = '女') {
this.name = name
this.gender = gender
this.report = () => { console.log(`我的名字叫${this.name}, 我的性别是${this.gender}`) }
}
const aZhen = new Person('阿珍', '女')
const aQiang = new Person('阿强', '男')
class
知识来源
a class can be defined in two ways: a class expression or a class declaration.
class有两种定义类型的方法:表达式定义和声明定义 ,对于实例属性(挂载在生成实例本身,而非挂载在构造函数上的属性)的定义,我们可以选择是/否前置声明。
知识来源
The fields can be declared with or without a default value. Fields without default values default to
undefined
. By declaring fields up-front, class definitions become more self-documenting, and the fields are always present, which help with optimizations.
虽然不对属性前置声明,也完全不影响功能的实现,但推荐大家前置声明所有属性,这样可以让我们的类型定义更加完整,增强代码的可读性和可维护性(比如可以清晰明了的看到这个类都有哪些实例属性,以及每个实例属性的默认值是什么)。
// 表达式定义
const Person = class {
// 前置声明属性,并给定初始值
gender = '女';
report = () => { console.log(`我的名字叫${this.name},性别是${this.gender}`) };
// 未给定初始值,属性默认为undefined
name;
constructor(name, gender) {
this.name = name
this.gender = gender || this.gender
}
}
// 声明定义
class Person {
// 和表达式定义内的代码一样
}
const aZhen = new Person('阿珍')
const aQiang = new Person('阿强','男')
知识来源
The
constructor
method is a special method of a class for creating and initializing an object instance of that class.
A constructor enables you to provide any custom initialization that must be done before any other methods can be called on an instantiated object.
If you don't provide your own constructor, then a default constructor will be supplied for you. If your class is a base class, the default constructor is empty
If your class is a derived class, the default constructor calls the parent constructor, passing along any arguments that were provided:
class中声明的constructor
用于创建对象实例,对于对象实例的访问行为,一定会发生在constructor
函数执行之后,因此用户可以在constructor
函数内定义需要前置执行的所有初始化行为。
若开发者未声明constructor函数,constructor的默认值有两种情况:
-
当前class未继承任何父类
constructor默认为空函数
constructor(){}
-
当前class继承自其他父类
constructor默认为调用父类constructor函数的函数
constructor(...args){ super(...args) }
结合前面对new
运算符的介绍,通过function和class创建实例的过程如下:
- 生成一个空对象
- 空对象的
[[Prototype]]
指向Person.prototype
- 执行function/class代码,并且执行过程中class/function内的
this
指向上述空对象 - 将最终的得到的对象赋值给aZhen / aQiang
// 所有实例的[[Prototype]]都指向构造函数的prototype
console.log(Object.getPrototypeOf(aZhen) === Person.prototype) // true
console.log(Object.getPrototypeOf(aZhen) === Object.getPrototypeOf(aQiang)) // true
aZhen.report() // 我的名字叫阿珍,性别是女
aQiang.report() // 我的名字叫阿强,性别是男
共享属性
按照上面的定义方式,我们会为每个对象实例都创建一个report
函数,100个实例,就会创建100个report
函数,毫无疑问,这是一种无意义的浪费,此时共享属性就派上用场了,只需要创建一个report
函数作为共享属性,就可以供所有同类型的对象实例共同访问了,共享属性有以下两种类型:
- 挂载在构造函数
prototype
上的共享属性 - 挂载在构造函数本身的共享属性(静态属性)
挂载在构造函数的prototype
上
根据前面的内容,我们可知:
- 当访问一个对象实例的某个属性时,首先会先在这个对象实例本身上找,如果没有找到,则会去他的 [[Prototype]] 上找
- 对象实例的 [[Prototype]] 指向构造函数的
prototype
因此将report
函数挂载在Person.prototype
上,所有通过Person
创建的对象实例都可以访问到该函数。
function
function Person(name, gender = '女') {
this.name = name
this.gender = gender
}
// 注意:不能用箭头函数,否则this将不再指向对象实例
Person.prototype.report = function() {
// this的指向不是本篇文章的重点,后面会单开一篇文章讲
// 读者在这里只需要知道原型对象的函数调用时,this指向调用函数的对象实例
console.log(`我的名字叫${this.name},我的性别是${this.gender}`)
}
Person.prototype.type = 'person'
aZhen.report() // 我的名字叫阿珍,性别是女
aQiang.report() // 我的名字叫阿强,性别是男
console.log(aZhen.type) // person
class
class同样可以用上面的方式挂载属性到prototype
class Person {
gender = '女'
name;
constructor(name, gender) {
this.name = name
this.gender = gender || this.gender
}
}
Person.prototype.report = function() {
console.log(`我的名字叫${this.name},我的性别是${this.gender}`)
}
Person.prototype.type = 'person'
除此之外,class还可以通过如下方式挂载函数到prototype
知识来源
Methods are defined on the prototype of each class instance and are shared by all instances. Methods can be plain functions, async functions, generator functions, or async generator functions.
class Person {
gender = '女'
name;
constructor(name, gender) {
this.name = name
this.gender = gender || this.gender
}
report () {
console.log(`我的名字叫${this.name},我的性别是${this.gender}`)
}
}
挂载在构造函数本身
function
function Person(name, gender = '女') {
// ...
}
Person.introduce = person => {
console.log(`这是一位名字叫${person.name}的${person.gender}性`)
}
Person.description = '人在生物学上通常指智人,偶尔也泛指人属的史前物种,为灵长目、人科的一部分,人属成员大致都由人猿/古猿演化而来'
console.log(Person.description) // 人在生物学上通常指智人,偶尔也泛指人属的史前物种,为灵长目、人科的一部分,人属成员大致都由人猿/古猿演化而来
Person.introduce(aZhen) // 这是一位名字叫阿珍的女性
// 构造函数生成的对象实例只能访问构造函数的prototype,无法访问构造函数本身
console.log(aZhen.description) // undefined
class
知识来源
The
static
keyword defines a static method or field for a class. Static properties (fields and methods) are defined on the class itself instead of each instance
class除了可以用与function相同的方式挂载属性到构造函数外,还可以通过static
关键字实现相同的功能。
class Person {
// ...
static introduce(person) {
console.log(`这是一位名字叫${person.name}的${person.gender}性`)
}
static description = '人在生物学上通常指智人,偶尔也泛指人属的史前物种,为灵长目、人科的一部分,人属成员大致都由人猿/古猿演化而来'
}
继承
知识来源
In programming, inheritance refers to passing down characteristics from a parent to a child so that a new piece of code can reuse and build upon the features of an existing one. JavaScript implements inheritance by using objects. Each object has an internal link to another object called its prototype. That prototype object has a prototype of its own, and so on until an object is reached with
null
as its prototype. By definition,null
has no prototype and acts as the final link in this prototype chain.
在Javascript中,继承是通过原型链来实现的,通过继承,可以将父级构造函数的特性传递给子级构造函数,从而基于已有代码实现功能的复用和改写。
function
function实现继承相对复杂一些,详细说明可以看看这篇文章
function inheritPrototype(subType, superType){
const prototype = Object.create(superType.prototype); // 创建父类原型的一个副本
prototype.constructor = subType; // 指定constructor属性指向构造函数本身
subType.prototype = prototype; // 将新创建的对象赋值给子类的原型
}
// 父类初始化
function Person(name, gender = '女') {
this.name = name
this.gender = gender
}
Person.prototype.report = function() {
console.log(`我的名字叫${this.name},我的性别是${this.gender}`)
}
// 子类初始化
function Child(name, gender, mom){
Person.call(this, name, gender);
this.mom = mom
}
// 基于父类prototype,创建子类prototype
inheritPrototype(Child, Person);
Child.prototype.momReport = function(){
console.log(`我的名字叫${this.name},我的妈妈是${this.mom.name}`)
}
const mom = new Person("美伢");
const aZhen = new Child("阿珍", '女', mom);
aZhen.report() // 我的名字叫阿珍,我的性别是女
aZhen.momReport() // 我的名字叫阿珍,我的妈妈是美伢
class
知识来源
The
extends
keyword is used in class declarations or class expressions to create a class as a child of another constructor (either a class or a function).
class对于继承的实现要简单的多,通过extends
关键字来实现子类的创建,其父类可以是function,也可以是class。
function Person(name, gender = '女') {
//...
}
class Child extends Person {
constructor(name, gender, mom) {
// 调用父类构造函数
super(name, gender)
this.mom = mom
}
momReport(){
console.log(`我的名字叫${this.name},我的妈妈是${this.mom.name}`)
}
}
知识来源
If there is a constructor present in the subclass, it needs to first call
super()
before usingthis
. Thesuper
keyword can also be used to call corresponding methods of super class.
关于super
,还有以下两个知识点:
- 如果子类声明了
constructor
函数,constructor
内调用this
之前一定要调用super
函数 super
还可以用于调用父类的共享属性
class Person {
constructor(name, gender) {
// ...
}
report () {
console.log(`我的名字叫${this.name},我的性别是${this.gender}`)
}
static introduce(person) {
console.log(`这是一位名字叫${person.name}的${person.gender}性`)
}
static description = '人在生物学上通常指智人,偶尔也泛指人属的史前物种,为灵长目、人科的一部分,人属成员大致都由人猿/古猿演化而来'
}
class Child extends Person {
constructor(name, gender, mom) {
this.mon = mom // ReferenceError,super 需要先被调用!
super(name, gender)
this.mom = mom
}
report(){
super.report()
console.log(`我的妈妈是${this.mom.name}`)
}
static introduce(child) {
super.introduce(child)
console.log(`他的妈妈名字是${child.mom.name}`)
}
static description = `${super.description},儿童泛指18岁以下的任何人`
}
const mom = new Person("美伢");
const aZhen = new Child("阿珍", '女', mom);
aZhen.report() // 我的名字叫阿珍,我的性别是女 我的妈妈是美伢
Child.introduce(aZhen) // 这是一位名字叫阿珍的女性 他的妈妈名字是美伢
console.log(Child.description) // 人在生物学上通常指智人,偶尔也泛指人属的史前物种,为灵长目、人科的一部分,人属成员大致都由人猿/古猿演化而来, 儿童泛指18岁以下的任何人
私有属性
知识来源
It's an error to reference private fields from outside of the class; they can only be read or written within the class body. By defining things that are not visible outside of the class, you ensure that your classes' users can't depend on internals, which may change from version to version
私有属性只能在class内使用,在class外调用会报错。
想象一下这种场景,我实现了一个工具函数准备发布到npm上公开,但是有一些属性,我已经预期到,随着功能迭代会发生变动(比如属性名从name1改为name2),为了降低维护成本,我不希望其他人可以访问到这些属性,那么我就可以将这些属性定义为私有属性。
私有属性通过#
前缀来创建,只有class支持该前缀。也就是说如果我们通过function定义类型的话,是不能通过#
前缀来创建私有属性的。
class Rectangle {
#height = 0;
constructor(height) {
this.#height = height;
}
getHeight () {
console.log(this.#height)
}
}
const rectObj = new Rectangle(100)
rectObj.getHeight() // 100
rectObj.#height // 报错,class外不能调用
知识来源
Private fields can only be declared up-front in a field declaration. They cannot be created later through assigning to them, the way that normal properties can.
注意:私有属性必须前置声明,不能像实例属性那样,在constructor
内在赋值的同时创建。
class Rectangle {
#height = 0;
constructor(height, width) {
this.#height = height;
// 没有前置声明,Uncaught SyntaxError
this.#width=width
}
}
结束语
好了,本菜鸡关于class和function只学习到这里,只能帮各位到这了,欢迎大家在评论区沟通交流,不过最近可能回复的不会很及时(因为公司的要求,要去研究其他东西了),大家先评论着,或者在评论区互相交流,等有时间了我再回来加入,我一定会回来的!(手动加载灰太狼语音包)哦对了,如果文章有帮助到你,帮我点个赞吧(谄媚脸 )~