镇楼图

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

JS笔记(四):面向对象、异常处理的更多相关文章

  1. [js笔记整理]面向对象篇

    一.js面向对象基本概念 对象:内部封装.对外预留接口,一种通用的思想,面向对象分析: 1.特点 (1)抽象 (2)封装 (3)继承:多态继承.多重继承 2.对象组成 (1)属性: 任何对象都可以添加 ...

  2. Java Script 读书笔记 (四) 面向对象编程

    1. 对象,属性 前面看到对象里删除属性一直疑惑,什么是对象,为什么属性可以删除, 我印象里的属性还是停留在property, 总想不明白为什么属性竟然能够删除.直到看到标准库才明白,原来对象就是py ...

  3. SpringMVC学习笔记四:SimpleMappingExceptionResolver异常处理

    SpringMVC的异常处理,SimpleMappingExceptionResolver只能简单的处理异常 当发生异常的时候,根据发生的异常类型跳转到指定的页面来显示异常信息 ExceptionCo ...

  4. python 全栈开发,Day52(关于DOM操作的相关案例,JS中的面向对象,定时器,BOM,client、offset、scroll系列)

    昨日作业讲解: 京东购物车 京东购物车效果: 实现原理: 用2个盒子,就可以完整效果. 先让上面的小盒子向下移动1px,此时就出现了压盖效果.小盒子设置z-index压盖大盒子,将小盒子的下边框去掉, ...

  5. 前端JavaScript(3)-关于DOM操作的相关案例,JS中的面向对象、定时器、BOM、位置信息

    小例子: 京东购物车 京东购物车效果: 实现原理: 用2个盒子,就可以完整效果. 先让上面的小盒子向下移动1px,此时就出现了压盖效果.小盒子设置z-index压盖大盒子,将小盒子的下边框去掉,就可以 ...

  6. ASP.NET MVC 学习笔记-7.自定义配置信息 ASP.NET MVC 学习笔记-6.异步控制器 ASP.NET MVC 学习笔记-5.Controller与View的数据传递 ASP.NET MVC 学习笔记-4.ASP.NET MVC中Ajax的应用 ASP.NET MVC 学习笔记-3.面向对象设计原则

    ASP.NET MVC 学习笔记-7.自定义配置信息   ASP.NET程序中的web.config文件中,在appSettings这个配置节中能够保存一些配置,比如, 1 <appSettin ...

  7. Java学习笔记之---面向对象

    Java学习笔记之---面向对象 (一)封装 (1)封装的优点 良好的封装能够减少耦合. 类内部的结构可以自由修改. 可以对成员变量进行更精确的控制. 隐藏信息,实现细节. (2)实现封装的步骤 1. ...

  8. Pthon面向对象-异常处理

    Pthon面向对象-异常处理 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.异常概述 1>.错误(Error) 逻辑错误: 算法写错了,例如加法写成了减法. 笔误: 例如 ...

  9. Data Visualization and D3.js 笔记(1)

    课程地址: https://classroom.udacity.com/courses/ud507 什么是数据可视化? 高效传达一个故事/概念,探索数据的pattern 通过颜色.尺寸.形式在视觉上表 ...

  10. C#可扩展编程之MEF学习笔记(四):见证奇迹的时刻

    前面三篇讲了MEF的基础和基本到导入导出方法,下面就是见证MEF真正魅力所在的时刻.如果没有看过前面的文章,请到我的博客首页查看. 前面我们都是在一个项目中写了一个类来测试的,但实际开发中,我们往往要 ...

随机推荐

  1. chatGPT-meta抗衡版本

    chatGPT-meta抗衡版本 链接:https://mp.weixin.qq.com/s/MbZTfVgxx221Eo9pl1h80w 内置 git代码 LLaMA 项目地址:https://gi ...

  2. Linux&Android相关常用命令汇总记录

    Linux&Android相关常用命令汇总记录 0@Linux&Android系统命令行下如何查看命令的帮助信息: command --help 1@在Linux系统中,设备分为三类, ...

  3. python 文件 写入

    import sys import os # 打印当前文件的路径 print(__file__) # 打印当前文件所在文件夹的路径 print(os.path.dirname(__file__)) # ...

  4. XSS - Cross Site Scripting

    origin url: https://www.synopsys.com/glossary/what-is-csrf.html#:~:text=Definition,has in an authent ...

  5. c++初始化和赋值的区别

    静态对象的声明及初始化不是赋值 声明(并缺省初始化)后再赋值 #include <iostream> #include <string> void fun(std::strin ...

  6. WPF 轨迹动画

    1.后台 public MainWindow() { InitializeComponent(); /// <summary> /// Window2.xaml 的交互逻辑 /// < ...

  7. Linux工作中最常用命令整理

    ls 命令:显示指定工作目录下之内容 ls -a # 显示所有文件夹,包含隐藏的. 和.. ls -l # 显示文件的详细信息,包含文件形态,权限,所属,大小,其实就是平常用的 ll ll -h # ...

  8. 更改材质uv

  9. Qt中的串口编程

    串行接口简称串口,也称串行通信接口或串行通讯接口(通常指COM接口),是采用串行通信方式的扩展接口.串行接口(Serial Interface) 是指数据一位一位地顺序传送,其特点是通信线路简单,只要 ...

  10. dp泄露

    DP泄露 选了三道与RSA的dp泄露有关的题,dp泄露算是比较有辨识度的题型. 目录 DP泄露 原理 ctfshow funnyrsa3 分析 解答 BUUCTF RSA2 分析 解答 [羊城杯 20 ...