文章目录
- JavaScript class类
- 基础概念
- 属性与方法相关概念
- 私有字段
- 类的name属性 返回类的名字
- 类的访问器方法
- super关键字
- new的过程中发生了什么
- extends继承 重写-重载
- 语法细节
- 类声明与类表达式
- 补充理解:let和const的作用域提升规则
- 类的继承
- 原型与隐式原型链
- 特殊原型链
- 原型链继承 - 子类的显式原型是父类的实例
- 构造函数继承 - 子类构造函数中调用父类构造函数
- 组合继承 原型链继承+构造函数继承
- 寄生组合继承
- 补充:Object.create原理
- extends 继承原理 (同寄生组合继承)
JavaScript class类
基础概念
- 每次
new
一个实例,constructor
方法就会调用一次。 - 在函数定义时,会自动往函数添加
prototype
属性,默认是一个空Object
对象,这个对象叫做显式原型对象。 - 实例对象的
__proto__
属性,叫做隐式原型属性/隐式原型,实例化对象时自动添加的,实例对象的隐式原型=构造函数的显式原型 - 原型对象有一个
constructor
属性,指向函数对象 class
可以看作构造函数的一个语法糖class Person{constructor(){} } console.log(typeof Person);//function console.log(Person===Person.prototype.constructor);//true
每个类都必须有一个
constructor
,如果没有显式声明,js 引擎会自动给它添加一个空的构造函数。
属性与方法相关概念
概念 | 定义位置 | 使用 | 特点 |
---|---|---|---|
原型方法 | constructor 外面 | 实例调用 | 定义在构造函数(类)的显式原型prototype 上 |
实例方法 | constructor 里面 | 实例调用 | 每new 一次,调用一次constructor ,constructor 内部会重新定义实例方法(改变this 指到实例),所以实例方法在每个实例上,方法同名但不是同一个。 |
静态方法 | static 标识符 修饰 | 类来调用 | 方法里的this 指向类本身,静态方法可以被子类继承 |
私有属性/方法 | # 标识符 修饰 | 只能在类的内部访问的方法和属性,外部不能访问 | 私有指的对类私有 |
ES5相关概念的写法
function A(x){}
A.prototype.show = function(){} // 原型方法// 实例方法
function A(x){this.x = x; // 实例属性this.show = function(){} // 实例方法
}
私有字段
私有字段包括私有实例字段和私有静态字段
说明
1.私有字段#名称
(hash),访问时也需要携带#
。
2.私有字段在构造器或调用子类的 super()
方法时被添加到类的实例中。
// 可以通过super方法
class ClassWithPrivateField {#privateField;constructor() {this.#privateField = 42; // 添加到类实例中}
}class SubClass extends ClassWithPrivateField {#subPrivateField;constructor() { super(); // 添加到子类的实例中this.#subPrivateField = 23;}
}new SubClass();
// SubClass {#privateField: 42, #subPrivateField: 23}
类的name属性 返回类的名字
class Person {}
Person.name // Person
类的访问器方法
通过getter
、setter
访问器函数,可以对读写进行拦截操作
class Person {constructor(name) {this._name = name}// 类的访问器方法get name() {return this._name}set name(val) {this._name = val}
}
super关键字
规定:super()
之前不能访问 this
有一种说法解释: 因为构造器是用来对实例初始化的,而子类实例在初始化之前要先初始化它的父类成分。那接下来为什么 JS 语言不可以隐式调用 super,而要交给开发者?因为 JS 是弱类型的,super 调用时的传参没法自动预判,没法代劳。
super()
:在子类构造器中,把super
当作一个函数来调用,创建对象并让其执行父类的构造器方法super.property
和super.method():
通过super
访问超类的原型属性和方法
new的过程中发生了什么
- 创建一个空对象,这个空对象就是返回的实例
- 类内部的
this
指向这个空对象 - 实例的隐式原型
__proto__
指向构造函数的显式原型prototype
- 执行构造器函数,为实例添加方法或属性
- 获取构造器函数执行的结果,如果构造器函数有返回对象,则将其返回。如果没有返回创建的实例。
function myNew(Fn,...args){let obj = {}; //1let obj.__proto__ = Fn.prototype;//2let result = Fn.apply(obj,args);//3return result instanceof Object ? result : obj;//4
}
extends继承 重写-重载
作用:用于创建一个类的子类
说明:父类的.prototype
必须是一个 Object
或者 null
重写:同名属性或方法,同名方法就可以了,不需要参数个数
语法细节
类声明与类表达式
- 类的声明的特点
- 将类的名称添加到当前作用域中
- 结尾的大括号不需要加分号
- 类似
let
和const
的作用域提升规则
- 类的表达式的特点
- 不会将类的名称添加到当前作用域中,赋值的结果是一个函数(类的构造函数)
//类的声明
class Class1{}
//类的表达式
let Color = class{};
//类的匿名表达式
let C = class Color2{};
conselo.log(Color2); //Color2 is not defined
console.log(typeof C)//"function"
补充理解:let和const的作用域提升规则
var
声明的变量会使声明被提升到顶部,let
和const
也存在变量提升,只是提升的方式不同。
var
变量提升:变量的声明提升到顶部,值为undefined
let
、const
变量提升: 变量声明提升到顶部,只不过将该变量标记为尚未初始化
//原代码
function fn(){console.log(answer); //undefinedvar answer=42;
}
//变量提升
function fn(){var answer;//声明提前console.log(answer); // 值为undefinedanswer=42;
}
let、const的暂时性死区
let
和 const
存在暂时性死区,代码执行过程中的一段时间内,在此期间无法使用标识符,也不能引用外层作用域的变量。
原因是也将声明提升到了顶部,只不过是标记该变量为尚未初始化
let answer;
function fn(){//如果此时没有将变量变量提升到这里,answer应该取外层answer的值// 提升到了这里并标记未尚未初始化console.log(answer); //Uncaught ReferenceError: Cannot access 'answer' before initializationlet answer=42;
}//理解暂时性死区是暂时的与时间相关
function temporalExample(){const f = ()=>{console.log(value)//这里不会报错}let value = 42;f(); //调用时,value已经声明
}
类的继承
继承对于JS来说就是父类拥有的方法和属性、静态方法等,子类也要拥有。
原型与隐式原型链
原型
函数在定义时会自动添加prototype属性显式原型属性,默认指向一个空Object对象,该对象称为显式原型对象。显式原型对象有一个constructor
属性,指向构造函数。
实例在创建对象时会自动添加__proto__
隐式原型属性,对象隐式原型的值=对应构造函数的显式原型的值
隐式原型链的概念
访问一个对象的属性时
1.现在自身属性中查找,找到返回
2.没有找到,再沿__proto__
这条链上找,找到返回
3.最终没找到,返回undefined
原型链的尽头是Object.prototype.__proto__ === null
原型链的作用
1.实现继承
2.数据共享,节约内存空间
特殊原型链
核心:实例的隐式原型指向构造函数显式原型
Function
可以看成是构造函数,也可以看成Function
的实例
Function.__proto__ === Function.prototype
Object
可以看成构造函数,也可以看成Function
的实例
Object.__ptoto__ === Function.prototype
原型链继承 - 子类的显式原型是父类的实例
本质:子类的显式原型是父类的实例
function Parent(){this.name = 'parent'this.play = [1,2,3]
}
function Child(){this.name = 'child';
}
Child.prototype = new Parent();//执行Parant构造器
Parent.prototype.id = '1';
let child1 = new Child(); //执行Child
console.log(child1.name)//child
console.log(child1.id)//1let child2 = new Child();
child1.play[0] = 2;
console.log(child2.play)//[2,2,3]
原型链:子类实例child
自身找 -> 子类的__proto__
找(这里是父类的实例) -> 子类__proto__
的__proto__
找(父类的显式原型)
- 优点
- 共享父类的实例属性/方法和原型上的属性和方法
- 缺点
- 父类的引用属性(
play
)会被所有子类共享,其中一个子类修改,其他子类也会受到影响 - 子类的实例不能给父类型构造函数传参(
Child.prototype = new Parent()这里已经调用了父类构造器方法
,全程只调用了一次所以没办法实例化子类的时候动态传参)
- 父类的引用属性(
构造函数继承 - 子类构造函数中调用父类构造函数
本质:子类构造函数中调用父类构造函数 - 只会继承构造器中的东西
function Parent(name){this.name = namethis.play = [1,2,3]
}
function Child(name){Parent.call(this,name);//执行Parent函数 - 子类的每个实例都会将父类中的属性复制一份。/* this.name = namethis.play = [1,2,3]*/
}
Parent.prototype.id = '1'var child1 = new Child('child'); //---ES5和ES6的区别 先创造子类的实例,执行构造器函数调用 Parent.call(this),继承父类实例的方法console.log(child1.name)//child
console.log(child1.id)//undefined
var child2 = new Child();
child1.play[0] = 2;
console.log(child2.play)//[1,2,3]
Parent.call(this,name) 第一次参数是指定函数Parent里的this 为参数this,name为Parent函数传递的参数
- 优点
- 可以在子类构造器中给父类构造函数传参
- 父类的引用对象不会共享
- 缺点
- 子类访问不了父类显式原型上的方法(从图里看子类和父类比较独立,只能继承构造器里的东西)
- 子类的实例每实例化一次,父类的构造器都会被调用一次
组合继承 原型链继承+构造函数继承
- 继承实例属性:子类的构造函数里调用父类的构造函数,防止父类引用类型被修改
- 继承原型上的属性和方法: 将父类的实例作为子类的原型,访问父类原型。
function Parent(name){this.name = namethis.play = [1,2,3]
}
function Child(name){//子类的每个实例都会将父类中的属性复制一份,访问时优先访问自己作用域中的Parent.call(this,name); //调用一次父类构造器
}
//继承原型上的属性和方法
Child.prototype = new Parent();//调用一次父类构造器
Child.prototype.constructor = Child;
这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性
- 优点
- 父类的方法可以复用
- 可以在子类构造器中向父类构造器传参
- 父类构造器中的引用属性不会被共享
- 缺点
- 调用了两次父类构造器,原型链上会存在两份相同的属性和方法(
child
实例有,child
实例的隐式原型上也有)
- 调用了两次父类构造器,原型链上会存在两份相同的属性和方法(
寄生组合继承
实例原型链:子类的实例可以拥有父类的方法,通过Son.prototype.__proto__ = Father.prototype
构造器原型链:子类可以拥有父类的静态方法,通过Son.__proto__=Father
/*
1.子类继承父类的属性
*/
function child(name){Person.call(this,name)
}
/*
2.子类可以看见父类的方法
先创建父类的实例
*/
// Object.create创建一个新对象,对象的隐式原型指向参数
Child.prototype = Object.create(Parent.prototype);
//Object.create创建的是一个新对象,所以需要显式指定constructor属性
Child.prototype.constructor = Child;/*
3.子类可以看见父类的静态方法
*/
Child.__proto__ = Parant;
补充:Object.create原理
作用:创建一个新对象,新对象的隐式原型指向参数
function create(proto) {function F(){}F.prototype = protoreturn new F()
}
案例
let obj = Object.create({name: 'johan'})
extends 继承原理 (同寄生组合继承)
核心代码
var Child = function (_Parent) {_inherits(Child, _Parent);function Child(name, age) {// Object.getPrototypeOf(Child) 获取child的隐式原型// Parent.call(this, name, age)var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Child).call(this, name, age));_this.name = name;_this.age = age;return _this;}return Child;
}(Parent);function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self;
}
_inherits
的逻辑与寄生组合继承相同
- 子类可以继承父类原型空间的属性与方法
- 子类可以继承父类的静态属性和方法
function _inherits(subClass, superClass) { // 如果有一个不是函数,则抛出报错if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } // 将 subClass.prototype 设置为 superClass.prototype 的实例subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); // Object.setPrototypeOf(obj, ) 指定对象obj的隐式原型为参数2 ,设置subClass的隐式原型为superClassif (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}