镇楼图

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. ansible笔记第三章(Ansible--tasks任务控制)

    (1)when判断语句 实践案例一.根据不同操作系统,安装相同的软件包 [root@m01 project1]# cat tasks_1.yml - hosts: oldboy tasks: - na ...

  2. 36.201——LTE物理层——总体描述物理层综述协议

    主要包括物理层在协议结构中的位置和功能,包括物理层4个规范36.211.36.212.36.213.36.214的主要内容和相互关系等 The radio interface is composed ...

  3. Scala集合排序

    Scala集合排序有三种方法:sorted.sortBy().sortWith() (1)sorted 对一个集合进行自然排序,通过传递隐式的Ordering源码中有两点值得注意的地方:1.sorte ...

  4. git 提交本地仓库到远程

    提交本地仓库到远程指定仓库 假如已经在远程github创建了项目 在本地创建了和github同样工程并添加了代码,如eclipse创建出工程并写了代码.要把这些代码提交到github git bash ...

  5. 模型admin 外键的相关操作

    ....@admin.register(MyModel)class MyModelAdmin(admin.ModelAdmin): def method(self, request, queryset ...

  6. macOS Big Sur 设置JAVA_HOME

    默认JAVA_HOME指向的是: /Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home 这个不是我们自己安装的jdk,另外本 ...

  7. oss上传,阿里云上传oss,带缩略图

    https://mp.weixin.qq.com/s/obL9JmzDYdkREEJIj_hVIQ 借用工具类 <dependency> <groupId>cn.xuyanwu ...

  8. Demo of canvas, canvas optimization and svg

    It used the canvas to draw the curves in the old project, and the client felt that it was vague, so ...

  9. 用C#的控制台程序写一个飞行棋项目

    using System; namespace 飞行棋项目 { class Program { ///1.画游戏头 ///2.初始化地图 ///把整数数组中数字编程控制台中显示的特殊字符显示的过程,就 ...

  10. -bash: ./mlnxofedinstall: /usr/bin/perl: bad interpreter: No such file or directory

    -bash: ./mlnxofedinstall: /usr/bin/perl: bad interpreter: No such file or directory 解决办法:安装相关的环境即可 输 ...