JavaScript Class类 | 类的继承 - 类的使用 -原型与原型链

文章目录

  • JavaScript class类
    • 基础概念
      • 属性与方法相关概念
        • 私有字段
        • 类的name属性 返回类的名字
        • 类的访问器方法
        • super关键字
      • new的过程中发生了什么
      • extends继承 重写-重载
    • 语法细节
      • 类声明与类表达式
        • 补充理解:let和const的作用域提升规则
    • 类的继承
      • 原型与隐式原型链
        • 特殊原型链
      • 原型链继承 - 子类的显式原型是父类的实例
      • 构造函数继承 - 子类构造函数中调用父类构造函数
      • 组合继承 原型链继承+构造函数继承
      • 寄生组合继承
        • 补充:Object.create原理
      • extends 继承原理 (同寄生组合继承)

JavaScript class类

基础概念

  1. 每次new一个实例,constructor方法就会调用一次。
  2. 在函数定义时,会自动往函数添加prototype属性,默认是一个空Object对象,这个对象叫做显式原型对象。
  3. 实例对象的__proto__属性,叫做隐式原型属性/隐式原型,实例化对象时自动添加的,实例对象的隐式原型=构造函数的显式原型
  4. 原型对象有一个constructor属性,指向函数对象
  5. class可以看作构造函数的一个语法糖
    class Person{
    	 constructor(){}
    }
    console.log(typeof Person);//function
    console.log(Person===Person.prototype.constructor);//true
    

每个类都必须有一个constructor,如果没有显式声明,js 引擎会自动给它添加一个的构造函数。

属性与方法相关概念

概念 定义位置 使用 特点
原型方法 constructor外面 实例调用 定义在构造函数(类)的显式原型prototype
实例方法 constructor里面 实例调用 new一次,调用一次constructorconstructor内部会重新定义实例方法(改变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
类的访问器方法

通过gettersetter访问器函数,可以对读写进行拦截操作

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.propertysuper.method(): 通过super访问超类的原型属性和方法

new的过程中发生了什么

  1. 创建一个空对象,这个空对象就是返回的实例
  2. 类内部的this指向这个空对象
  3. 实例的隐式原型__proto__指向构造函数的显式原型prototype
  4. 执行构造器函数,为实例添加方法或属性
  5. 获取构造器函数执行的结果,如果构造器函数有返回对象,则将其返回。如果没有返回创建的实例。
function myNew(Fn,...args){
	let obj = {}; //1
	let obj.__proto__ = Fn.prototype;//2
	let result = Fn.apply(obj,args);//3
	return result instanceof Object ? result : obj;//4
}

extends继承 重写-重载

作用:用于创建一个类的子类
说明:父类的.prototype必须是一个 Object 或者 null

重写:同名属性或方法,同名方法就可以了,不需要参数个数

语法细节

类声明与类表达式

  • 类的声明的特点
    • 将类的名称添加到当前作用域中
    • 结尾的大括号不需要加分号
    • 类似letconst的作用域提升规则
  • 类的表达式的特点
    • 不会将类的名称添加到当前作用域中,赋值的结果是一个函数(类的构造函数)
//类的声明
class Class1{}
//类的表达式
let Color = class{};
//类的匿名表达式
let C = class Color2{};
conselo.log(Color2); //Color2 is not defined
console.log(typeof C)//"function"
补充理解:let和const的作用域提升规则

var声明的变量会使声明被提升到顶部,letconst也存在变量提升,只是提升的方式不同。

  • var变量提升:变量的声明提升到顶部,值为undefined
  • letconst变量提升: 变量声明提升到顶部,只不过将该变量标记为尚未初始化
//原代码
function fn(){
	console.log(answer); //undefined
	var answer=42;
}
//变量提升
function fn(){
	var answer;//声明提前
	console.log(answer);  // 值为undefined
	answer=42;
}

let、const的暂时性死区
letconst存在暂时性死区,代码执行过程中的一段时间内,在此期间无法使用标识符,也不能引用外层作用域的变量。

原因是也将声明提升到了顶部,只不过是标记该变量为尚未初始化

let answer;
function fn(){
	//如果此时没有将变量变量提升到这里,answer应该取外层answer的值
	// 提升到了这里并标记未尚未初始化
	console.log(answer); //Uncaught ReferenceError: Cannot access 'answer' before initialization
	let 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

JavaScript Class类 | 类的继承 - 类的使用 -原型与原型链_第1张图片

原型链继承 - 子类的显式原型是父类的实例

本质:子类的显式原型是父类的实例

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)//1

let child2 = new Child();
child1.play[0] = 2;
console.log(child2.play)//[2,2,3]

原型链:子类实例child自身找 -> 子类的__proto__找(这里是父类的实例) -> 子类__proto____proto__找(父类的显式原型)
JavaScript Class类 | 类的继承 - 类的使用 -原型与原型链_第2张图片

  • 优点
    • 共享父类的实例属性/方法和原型上的属性和方法
  • 缺点
    • 父类的引用属性(play)会被所有子类共享,其中一个子类修改,其他子类也会受到影响
    • 子类的实例不能给父类型构造函数传参(Child.prototype = new Parent()这里已经调用了父类构造器方法,全程只调用了一次所以没办法实例化子类的时候动态传参)

构造函数继承 - 子类构造函数中调用父类构造函数

本质:子类构造函数中调用父类构造函数 - 只会继承构造器中的东西

function Parent(name){
    this.name = name
    this.play = [1,2,3]
}
function Child(name){
    Parent.call(this,name);//执行Parent函数 - 子类的每个实例都会将父类中的属性复制一份。
    /* 
     this.name = name
     this.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函数传递的参数

JavaScript Class类 | 类的继承 - 类的使用 -原型与原型链_第3张图片

  • 优点
    • 可以在子类构造器中给父类构造函数传参
    • 父类的引用对象不会共享
  • 缺点
    • 子类访问不了父类显式原型上的方法(从图里看子类和父类比较独立,只能继承构造器里的东西)
    • 子类的实例每实例化一次,父类的构造器都会被调用一次

组合继承 原型链继承+构造函数继承

  1. 继承实例属性:子类的构造函数里调用父类的构造函数,防止父类引用类型被修改
  2. 继承原型上的属性和方法: 将父类的实例作为子类的原型,访问父类原型。
function Parent(name){
  this.name = name
  this.play = [1,2,3]
}
function Child(name){
  //子类的每个实例都会将父类中的属性复制一份,访问时优先访问自己作用域中的
  Parent.call(this,name); //调用一次父类构造器
}
//继承原型上的属性和方法
Child.prototype = new Parent();//调用一次父类构造器
Child.prototype.constructor = Child;

这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性
JavaScript Class类 | 类的继承 - 类的使用 -原型与原型链_第4张图片

  • 优点
    • 父类的方法可以复用
    • 可以在子类构造器中向父类构造器传参
    • 父类构造器中的引用属性不会被共享
  • 缺点
    • 调用了两次父类构造器,原型链上会存在两份相同的属性和方法(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;

JavaScript Class类 | 类的继承 - 类的使用 -原型与原型链_第5张图片

补充:Object.create原理

作用:创建一个新对象,新对象的隐式原型指向参数

function create(proto) {
    function F(){}
    F.prototype = proto
    return new F()
}

案例

let obj = Object.create({name: 'johan'})

JavaScript Class类 | 类的继承 - 类的使用 -原型与原型链_第6张图片

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的逻辑与寄生组合继承相同

  1. 子类可以继承父类原型空间的属性与方法
  2. 子类可以继承父类的静态属性和方法
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的隐式原型为superClass
    if (superClass) 
        Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 
}

JavaScript Class类 | 类的继承 - 类的使用 -原型与原型链_第7张图片

你可能感兴趣的:(javascript,原型模式,开发语言)