镇楼图

Pixiv:torino



六、JS中的面向对象

类(class)

博主视为你已拥有相关基础,这里不再赘述相关概念

类的语法如下,class在本质上是function,可以说class只是针对构造器的一种语法糖,但却不用像编写构造器那样麻烦。上一章博主给出了例子,需要编写prototype、constructor等内容,而且是分离开写的,class可以只在一个代码块内编写完成。其中constructor去编写构造器,若无构造器class会自动创建。constructor外可以编写属性,直接作用于要构造的对象上,而方法是作用于原型上。此外关于this指针,若要获取对象的属性,除了类中定义时不需要写this,其他的方法、构造器均需要this来获取

class MyClass {
  prop = value;
  ["Test"] = value;
  //属性作用于对象
  constructor(...) {}
  //构造器,编写function MyClass(...){}
  method(...) {}
  [Symbol.toStringTag]() {}
  get something(/**/) {}
  set something(value) {}
  //方法作用于原型
  //访问器属性也作用于原型,但属性something会同时出现在对象和原型上
}

class相当于封装了构造器、原型相关的编写,它存在一些约束

约束1——class内部有一属性[[IsClassConstructor]]:true,导致必须通过new创建实例,而在构造函数中可以使用new.target使得可以忽略new。(哪怕constructor内写了new.target相关处理也是无用,它是通过[[IsClassConstructor]]来判定的)

约束2——class定义的方法默认enumerable:false,如果选择构造函数必须要手动设置(毕竟大部分实际应用只希望枚举数据而不是函数)

约束3——class内代码默认使用use "strict",严格模式目前博主暂未给出解释,但严格模式在很多地方都做了好的约束

类表达式:类似于函数,它也有两种不同的定义方式

let MyClass1 = class{/**/};
let MyClass2 = class Inner{/**/};//作用参考NFE

类继承

JS提供了extends语法。当创建某个类的对象时,它会先执行constructor,若这个类是继承类,继承类的constructor必须存在super且只能位于constructor的第一行。super即创建父类的一个对象,子类的对象的[[Prototype]]会设置为创建的父类的对象。

如某个继承对象存在继承链A→B→C→D,那么虽然创建A的对象实际上还创建了B、C、D的三个对象,其中A的方法在B的对象中,B的方法在C的对象中,C的方法在D的对象中,D的方法在某个Object的对象中,而Object还存在Object.prototype

如果是继承类其构造器(派生构造器,derived constructor)内部存在特殊属性[[ConstructorKind:"derived"]]表明这是继承类的构造器,必须存在super

class Rect{
	constructor(a=3,b=4){
        this.a = a;
        this.b = b;
    }
}
class Square extends Rect{
	constructor(side=5){
        //必须存在super且必须位于第一行
        super(side,side);
        this.side = side;
    }
}
let s = new Square;
console.log(s);

而类继承也不局限于类,它可以是一个任意的表达式,只要保证extends后是类即可,因此可以使用函数来创建一个复杂化的类

假设一个游戏的怪物有龙、人、史莱姆三种类型,那么可以设计一个函数去生成父类,而不是再去编写

function monsterClassGenerator(str){
    let r = new Map([["Dragon",{name:"特征1",hp:"high",def:"high",atk:"high"}],["Human",{name:"特征1",hp:"low",def:"low",atk:"medium"}],["Slime",{name:"特征1",hp:"low",def:"medium",atk:"low"}]]);
    if(r.has(str)){
        return class{
            constructor(){
                this.tag = str;
                this.feature = r.get(str);
            }
            getTag(){
                return this.tag;
            }
            attack(){console.log("普通攻击")}
        }
    }
    return class{tag = undefined;};
}
class FireDragon extends monsterClassGenerator("Dragon"){
    //继承函数生成的类
    /*...*/
    tech1(){console.log("释放一技能")}
    tech2(){console.log("释放二技能")}
}

重写

super另外一个作用就是去索引父类(原理上是索引原型),可以通过super来重写方法

class A{
	Test(){console.log("A");}
}
class B extends A{
    //备注:super仅能用在class内
	Test(){super.Test();console.log("B");}
}
new B().Test();

除了重写constructor、方法外,重写属性看起来非常奇怪。如下代码,属性被覆盖后父类方法使用this却只使用其本身的,而方法可以正常指向派生类的方法

class A{
	test = "test1";
    func(){console.log("A");}
    constructor(){console.log(this.test);this.func();}
}
class B extends A{
	test = "test2";
    func(){console.log("B");}
}
new A();//tset1,A
new B();//test1,B

这样的原因是由于初始化的顺序问题,创建一个子类对象它会优先创建父类(若父类还有父类会继续向上创建),初始化父类后才会初始化子类。上面代码仅限于constructor,在普通方法不会引发属性被错误使用的情况。另外可以使用访问器属性,它虽然形式上是属性,但本质上是函数可以避免被错误使用

super的原理

直接采用获取proto的形式去实现super是不可能的,如下代码,B去运行A的方法确实可行,因为this指向B其原型为A,恰好可以执行A的代码且数据为B的。但C去运行却报错了。当C去执行B的方法时,此时this依然是指向C的而不会变化到B,从而导致一个无限调用B的函数最终栈溢出

let A = {
    data: 1,
	func(){console.log(this.data)}
};
let B = {
    __proto__: A,
    data: 2,
    func(){Object.getPrototypeOf(this).func.call(this);}
};
let C = {
    __proto__: B,
    data: 3,
    func(){Object.getPrototypeOf(this).func.call(this);}
};
B.func();//成功运行
C.func();//异常

JS为函数添加了内部属性[[HomeObject]],当函数是类或对象的方法时,[[HomeObject]]永久指向该对象。super可以通过原型的[[HomeObject]]来获取方法。它与this的区别是this会随着上下文发生变化,[[HomeObject]]是永久绑定的,但违反了方法的自由性

let A = {
    data: 1,
	func(){console.log(this)}
};
let B = {
    __proto__: A,
    data: 2,
    func(){super.func();}
};
let C = {
    __proto__: B,
    data: 3,
    func(){super.func();}
};
B.func();
C.func();

但[[HomeObject]]仅用作super,随意被直接使用可能导致异常,如下代码,原本是想借用rabbit的方法,但却输入错误信息

let animal = {
  sayHi() {
    alert(`I'm an animal`);
  }
};

let rabbit = {
  __proto__: animal,
  sayHi() {
    super.sayHi();
  }
};

let plant = {
  sayHi() {
    alert("I'm a plant");
  }
};

let tree = {
  __proto__: plant,
  sayHi: rabbit.sayHi
  // (*)rabbit中super指向animal
};

tree.sayHi();  // I'm an animal

虽然对象里函数、变量统称为数据属性,大部分情况下也没什么问题,但JS中直接存储函数才会设置[[HomeObject]],变量去存储函数不设置[[HomeObject]]可能导致super出现问题

class A{
	func = function(){console.log("A")}
}
class B extends A{
    func = function(){super.func();}
}
new B.func();//错误,super无法使用

静态成员

在之前使用构造函数时,若要创建额外的静态成员必须要单独写,而类中提供了static关键字直接编写静态成员。静态方法下的this即class本身,如果有需要的话还可以用静态方法改变类本身

class MyClass{
	//...
    static staticAttribute = 0;
    static staticMethod(/*...*/){/*...*/}
}
//调用
console.log(MyClass.staticAttribute);
MyClass.staticMethod(/*...*/);

私有成员

JS特有的访问器属性支持一些对成员的控制。可以只用getter而不用setter完成只读的控制,使用getter、setter完成写入受限的属性。除了访问器属性外对于类里存在私有成员的支持,只需要成员名前加#即可。私有成员即类外无法调用只能内部调用

calss MyClass{
	//...
    #privateAttribute = 0;
    #privateMethod(/*...*/){/*...*/}
}

但JS私有成员与其他变量不同的是私有成员与其他成员的命名不会冲突,此外私有成员无法使用this["#xxx"]的语法形式

class Test{
    #test = "test";
    get1(){return this.#test;}
    get test(){
        return this.#test;
    }
    set test(value){
        this.#test = this.test;
    }
    get2(){/*console.log(this["#test"]);*/return this.test;}
}
let test = new Test;
console.log(test);

内建类

和Object一样所有内建对象也可当作内建类,若内建类功能不足以满足需求却非常接近可以extends制定某个内建类的子类来满足。但内建类的继承与普通类的继承稍有区别,加入A extends B。一般来说A.prototype的[[Prototype]]为B.prototype,A的[[Prototype]]为B,即A不仅继承B的非静态成员还继承B的静态成员。但若B是内建类,A没有[[Prototype]]无法继承B的静态成员

class Test extends Array{}

let a = new Test;
console.log(Test.isArray(a));// Error

虽然无法使用静态方法,但Symbol中的静态getter:species允许子类覆盖对象的默认构造函数,此时就可以“继承”静态成员了

class Test extends Array{
    test(){console.log("test")}
	static get [Symbol.species](){return Array;}
}
let a = new Test(1,2,3);
console.log(Test.isArray(a));
console.log(a);

不过species一般不太可能使用,它会导致生成的对象与一开始的不符合,若没用species则依然保持其子类

class Test extends Array{
    test(){console.log("test")}
    //static get [Symbol.species](){return Array;}
}
let a = new Test(1,2,3);
console.log(a);//Test类
a = a.map(x => x*2);
console.log(a);//Test类
//若写入species则为Array类

instanceof

instanceof是用来判断对象是否隶属于某个类(或某个类的子类)的运算符,和typeof一样重要,用来作类型校验

obj instanceof Class
class Test extends Array{}
console.log(new Test instanceof Array);
//true,是Array的子类
console.log(new Test instanceof Object);
//true

默认情况下会考虑其原型链,如上代码还可以隶属于Object,但实际应用可能不需要这么广的判定范围,Symbol中有静态方法hasInstance可以改变判定的逻辑

class Test extends Array{
	static [Symbol.hasInstance](instance) {
		//instance是指当前对象
		return Array.isArray(instance);
	}
}
let a = new Test;
console.log(a instanceof Test);//true
console.log(a instanceof Object);//false

instanceof的原理是Class的prototype是否为obj原型链上的一个

obj.__proto__ === Class.prototype?
obj.__proto__.__proto__ === Class.prototype?
obj.__proto__.__proto__.__proto__ === Class.prototype?
...

除了typeof、instanceof外还可使用Object.prototype.toString而且更加通用也可结合toStringTag自定义标签

class Test extends Array{}
let a = new Test;
console.log(typeof a);
console.log(a instanceof Test);
console.log(Object.prototype.toString.call(a));

Mixin模式

JS是单继承,但却可以有类似于接口的Mixin模式实现“多继承”。构建对象内含属性或方法(一般只含方法),然后使用Object.assign将mixin复制到类的prototype中即可

let mixin = {
    test1: "test",
	test2(){console.log("test")}
};
class Test{}
Object.assign(Test.prototype, mixin);
new Test().test2();
console.log(new Test().test1);


七、异常处理

try-catch

可以使用try-catch来捕获异常并处理,当try中的代码发生异常时会转向catch进行相关处理以保证程序的健壮性,error参数包含了错误信息

try{
	//...
}catch(error){
	//捕获错误后的处理
}

它和其他大多数编程语言类似,它只能处理运行时的错误(简称异常),而对解析时就遇到的错误(JS中只有语法错误SyntaxError)会直接报错。JS中有Error内建对象存储了各种错误类型,最基本的错误有SyntaxError、TypeError、URIError、ReferenceError、RangeError、InternalError、EvalError,当然也可以自定义错误

try{
    {{//引发语法错误
}catch(error){
	console.log("Error!");
}

try-catch是同步执行的,如果有延时后才错误的不会发现,若在异步的代码中保持异常处理必须在异步的代码内部使用try-catch

try {
  setTimeout(function() {
    error;
  }, 1000);
} catch (err) {
  console.log( "不会检查出而直接报错" );
}

setTimeout(()=>{
	try{
    	error;
    }catch(error){
    	console.log("发现错误");
    }
},1000);

catch中也可以忽略Error对象,因为可能不需要处理Error对象

try{
	//...
}catch{
	console.log("Error!");
}

Error

Error也是内建对象,其中有name、message、cause属性(已忽略非标准属性),方法有Error.prototype.toString,该方法会返回name、message(若为空字符串则不显示)

name是语义性的标签,表明是什么类型的异常,默认为“Error”,用户可自定义

message用于简短描述该类错误,为字符串类型,默认空字符串

cause用于给定该类错误的具体原因,它可以是任何值

■构造Error

Error();
Error(message);
Error(message, {cause});
//Error构造可以忽略new
//构造Error无法指定name属性
function select(index){
    if(index < 0 || !Number.isInteger(index)){
        let e = Error("输入异常",{cause: `\n异常数据: ${index}\n可能原因: 输入小于零或非整数`});
    	  throw e + e.cause;
    }
    console.log`选了${index}`;
}
select(-2);

throw

throw可以抛出一个Error对象,一般搭配try-catch使用,throw会引导至catch代码块

let json = '{ "age": 30 }';
try {
  let user = JSON.parse(json);
  if (!user.name) {
    throw new SyntaxError("没有name");
  }
  console.log( user.name );
} catch(err) {
  console.log( "JSON Error: " + err.message );
}

但try内的代码中会接收任何错误,如果需要锚定错误类型,可以作类型判断

try {
	//...
}catch(err){
	if(err instanceof ReferenceError){
    	console.log("ReferenceError");
    }else{
    	console.log("OtherErroe");
    }
}

try-catch也可以嵌套实现不同层级的异常处理,如你构建了数据,它可能会检查数据是否有异常1但不会处理可能的异常2,它只会在数据应用到某个功能上时才会处理

function 功能(data){
	try{
        //...
    }catch(err){
    	console.log("err");
    }
}
function 创建数据(){
    let data = null;
    try{
    	//...
        return data;
    }catch(err){
    	if(err instanceof Error1){
        	console.log("引发异常1");
        }else{
        	throw err;
        }
    }
}
功能(创建数据());
//如果引发其他异常将会throw到[功能]上

finally

try-catch可以加上finally子句,不管是否出错最后都会执行finally子句。如你想做一个测量函数执行时间的函数,但函数执行时可能报错,但不管是否报错你都想直到测量的时间,那么测量时间的代码可以写在finally中

try {
  console.log( 'try' );
  if (confirm('Make an error?')) BAD_CODE();
} catch (err) {
  console.log( 'catch' );
} finally {
  console.log( 'finally' );
}

在函数中不管是否在try、catch中提前return、throw,finally都会执行,且finally优先执行

function func() {
  try {
    return 1;
  } catch (err) {
    /* ... */
  } finally {
    console.log( 'finally' );
  }
}
console.log( func() );
//优先输出finally再输出1

而且也可以不用catch完全try-finally结构,如果出现异常直接跳出该结构但也会执行finally

function measure(func,count,...args){
	let start = new Date();
    try{
        for(let i = 0;i < count;i++){
        	func.call(this,...args);
        }
    }finally{
        let end = new Date();
        return end-start;
    }
}
function gcd(a,b){
    if(tyepof(a) !== "number" || typeof(a) !== "number"){
        throw Error("错误!");
    }
	return (b == 0) ? a : gcd(b,a%b);
}
console.log("执行1w次gcd所需时间:"+measure(gcd,10000,123456,654321)+"ms");
console.log("出错也可正常运行:"+measure(gcd,1000,-5,7)+"ms");

JS自带Error类型

(1)SyntaxError语法错误,try-catch无法捕获语法错误(因为不是运行时错误类型)

(2)ReferenceError引用错误,当不存在的变量被引用时发生该错误

try{
    func();//不存在func
}catch(err){
	console.log(err);
}

(3)TypeError类型错误,当函数参数类型不符或错误使用某类型数据时发生该错误

try{
    console.log(Object.fromEntries([1,2,3]));
    //fromEntries要求二元素数组
}catch(err){
    console.log(err);
}

(4)RangeError范围错误,简单来说就是溢出,当可迭代对象长度过长或是调用栈过长时发生该错误

function func(){
	func();
}
try{
	func();
}catch(err){
    console.log(err);
}

(5)URIError,当调用JS内置的URI相关函数时若有错误会触发该错误,URI相关函数有decodeURI、decodeURIComponent、encodeURI、encodeURIComponent

(6)EvalError,当调用eval函数时若有错误会触发该错误,此类型错误不再抛出仅为兼容性而存在

包装异常

若对异常专门设计,异常经常呈层次结构

class Exception extends Error{
	constructor(msg){
        super(msg);
        this.name = "Exception";
    }
}
class IOException extends Exception{
    constructor(msg){
        super(msg);
        this.name = "IOException";
    }
}
class FileNotFoundException extends IOException{
    constructor(msg){
        super(msg);
        this.name = this.constructor.name;
        //建议使用constructor提高通用性
    }
}

在实际使用中可能会有不同层次的异常,一般检测类型时应当使用instanceof因为其可以校验任何子类

try{
    throw new FileNotFoundException("");
}catch(err){
	if(err instanceof Exception){
        //Exception体系
        console.log(err.name);
    }else if(err instanceof Error体系2){
        console.log(err.name);
    }else{
    	console.log(err.name);
    }
}

但上述体系显然存在一个缺陷,如果不同类型的Error过多可能导致实际捕获时过于繁琐,下面引入了“包装异常”的方法。ReadError相当于包装任何非运行时的异常,使得实际判断更容易。除了ReadError外还需要编写一个集中处理异常的函数read用于生成ReadError

class ReadError extends Error {
  constructor(msg, cause) {
    super(msg);
    this.cause = cause;
    this.name = this.constructor.name;
  }
}
function read(data){
	//...执行代码
    try{
        //...尝试捕获一类型Error
    }catch(err){
    	if(err instanceof Error1){
        	throw new ReadError("xxx",err);
            //err作为cause
        }
    }
    try{
        //...尝试捕获二类型Error
    }catch(err){
    	if(err instanceof Error2){
        	throw new ReadError("xxx",err);
        }
    }
    //...
}
//当
try{
	read(data);
}catch(err){
    //实际判断时仅需判断ReadError和其他Error
	if(err instanceof ReadError){
    	//...
    }else{
    	//...
    }
}


参考资料

[1] 《JavaScrpit DOM 编程艺术》

[2] MDN

[3] 现代JS教程

[4] 黑马程序员 JS pink

posted on 2023-02-15 22:23  摸鱼鱼的尛善  阅读(58)  评论(0编辑  收藏  举报