JVM 全称 Java Virtual Machine,也就是我们耳熟能详的 Java 虚拟机。它能识别 .class后缀的文件,并且能够解析它的指令,最终调用操作系统上的函数,完成我们想要的操作。
可能有部分小伙伴学习过C++,C++开发出来的程序,编译成二进制文件后,就可以直接执行了,操作系统是能够识别的。
但是咱们开的的Java程序就不一样了,使用javac命令编译出来的的.class文件之后,操作系统是不能识别的,需要对应JVM去做一个转换后,操作系统才能识别。
我们为什么不能像 C++ 一样,直接在操作系统上运行编译后的二进制文件呢?而非要搞一个处于程序与操作系统中间层的虚拟机呢?
这就是 JVM的过人之处了。大家都知道,Java 是一门抽象程度特别高的语言,提供了自动内存管理等一系列的特性。这些特性直接在操作系统上实现是不太可能的,所以就需要JVM 进行做一系列的转换。
大家一开始学Java的时候,就知道有个Write Once, Run Everywhere。就是我们编写了一个java文件经过编译成.class文件后,可以在各种系统中进行运行。
其实这里是有个前提的,我们需要在对应操作系统中安装对应的JVM,然后我们的.class文件就能运行了。
比如:Windows操作系统有对应的JDK安装版本、Linux也有对应的JDK安装版本等。
Java Development Kit (JDK) 是Sun公司(已被Oracle收购)针对Java开发员的软件开发工具包。自从Java推出以来,JDK已经成为使用最广泛的Java SDK(Software development kit)。
经非官方调查,目前JDK8是使用者最多的版本。
JDK14将在4月和7月收到安全更新,然后由9月到期的非LTS版本的JDK 15取代。JDK14包括16项新功能,例如JDK Flight Recorder事件流,模式匹配和开关表达式等特征。
从JDK9之后,Oracle采用了新的发布周期:每6个月发布一个版本,每3年发布一个LTS版本。JDK14是继JDK9之后发布的第四个版本, 该版本为非LTS版本,最新的LTS版本为JDK11。
下面是JDK版本情况
这个混个眼熟就行,随时关注JDK版本更新和新特性。
官网地址:https://www.oracle.com/java/
关于JDK安装这里就省略。
上面已经说过JDK和JVM的相关概念,
JRE全程Java Runtime Environment,是运行基于Java语言编写的程序所不可缺少的运行环境。也是通过它,Java的开发者才得以将自己开发的程序发布到用户手中,让用户使用。
三者到底是什么关系呢?
关于三者关系请看官网
https://docs.oracle.com/javase/8/docs/index.html
JDK中包含JRE,也包括JDK,而JRE也包括JDK。范围关系:JDK>JRE>JVM
编写一个HelloWorld.java文件
内容就是一个Java入门
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello world");
}
}
打开CMD,进入当前目录,使用命令
javac HelloWorld.java
就编译出HelloWorld.class
这个javac命令过程到底干了些什么呢?
javac背后大致做了这些操作
这个流程
读取源代码,一个字节一个字节的读取,找出其中我们定义好的关键字(如Java中的if、else、for、while等关键词,识别哪些if是合法的关键词,哪些不是),这就是词法分析器进行词法分析的过程,其结果是从源代码中找出规范化的Token流。
通过语法分析器对词法分析后Token流进行语法分析,这一步检查这些关键字组合再一次是否符合Java语言规范(如在if后面是不是紧跟着一个布尔判断表达式),词法分析的结果是形成一个符合Java语言规范的抽象语法树。
通过语义分析器进行语义分析。语音分析主要是将一些难懂的、复杂的语法转化成更加简单的语法,结果形成最简单的语法(如将foreach转换成for循环 ,好有注解等),最后形成一个注解过后的抽象语法树,这个语法树更为接近目标语言的语法规则。
通过字节码生产器生成字节码,根据经过注解的语法抽象树生成字节码,也就是将一个数据结构转化为另一个数据结构。最后生成我们想要的.class文件。
我只用的是Notepad++,选中文本→插件→Converter→ASCII->HEX
class文件的开头就是
CAFEBABE
想要学习这里的十六进制的字节码的含义可以参考
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
javap是 Java class文件分解器,可以反编译(即对javac编译的文件进行反编译),也可以查看java编译器生成的字节码。
新建一个User.java源文件,经过javac编译后,生成User.classs。
package com.tian.demo.test;
public class User {
private int age = 22;
private String name = "tian";
public int addAge() {
return age = age + 1;
}
public static void main(String[] args) {
}
}
使用javap命令
javap -v User.class >log.txt
打开log.txt
Classfile /D:/workspace/new/demo/src/main/java/com/tian/demo/test/User.class
Last modified 2020-11-5; size 441 bytes
MD5 checksum 2fa72d3f53bd9f138e0bfae82aba67e3
Compiled from "User.java"
public class com.tian.demo.test.User
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#21 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#22 // com/tian/demo/test/User.age:I
#3 = String #23 // tian
#4 = Fieldref #5.#24 // com/tian/demo/test/User.name:Ljava/lang/String;
#5 = Class #25 // com/tian/demo/test/User
#6 = Class #26 // java/lang/Object
#7 = Utf8 age
#8 = Utf8 I
#9 = Utf8 name
#10 = Utf8 Ljava/lang/String;
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 addAge
#16 = Utf8 ()I
#17 = Utf8 main
#18 = Utf8 ([Ljava/lang/String;)V
#19 = Utf8 SourceFile
#20 = Utf8 User.java
#21 = NameAndType #11:#12 // "<init>":()V
#22 = NameAndType #7:#8 // age:I
#23 = Utf8 tian
#24 = NameAndType #9:#10 // name:Ljava/lang/String;
#25 = Utf8 com/tian/demo/test/User
#26 = Utf8 java/lang/Object
{
public com.tian.demo.test.User();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 22
7: putfield #2 // Field age:I
10: aload_0
11: ldc #3 // String tian
13: putfield #4 // Field name:Ljava/lang/String;
16: return
LineNumberTable:
line 3: 0
line 4: 4
line 5: 10
public int addAge();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: aload_0
2: getfield #2 // Field age:I
5: iconst_1
6: iadd
7: dup_x1
8: putfield #2 // Field age:I
11: ireturn
LineNumberTable:
line 8: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 13: 0
}
SourceFile: "User.java"
魔数与class文件版本
常量池
访问标志
类索引、父类索引、接口索引
字段表集合
方法表集合
属性表集合
然后JVM就可以读取这个User.class文件进行解析等一系列的操作。
以上就是我们的Java文件到class文件。
后续还会更新一系列JVM相关文章,敬请期待~
CMAScript 6 提供了更接近传统语言的写法,新引入的class关键字具有正式定义类的能力。类(class)是ECMAScript中新的基础性语法糖结构,虽然ECMAScript 6类表面上看起来可以支持正式的面向对象编程,但实际上它背后使用的仍然是原型和构造函数的概念,让对象原型的写法更加清晰、更像面向对象编程的语法。
定义类也有两种主要方式:类声明和类表达式。这两种方式都使用class关键字加大括号:
kotlin
复制代码
// 类声明 class Person {} // 类表达式 const TestPerson=class {}
注意:函数声明和类声明之间的一个重要区别在于,函数声明会提升,类声明不会。需要先声明类,然后再访问它,否则就会出现ReferenceError,如:
arduino
复制代码
const test=new Person(); // ReferenceError: Person is not defined class Person {}
另一个跟函数声明不同的地方是,函数受函数作用域限制,而类受块作用域限制:
javascript
复制代码
{ function FunctionDeclaration () {} class ClassDeclaration {} // 使用var 声明 var VarClass=class {} // 使用let/const 声明 let LetClass=class {} } console.log(FunctionDeclaration) // FunctionDeclaration () {} console.log(ClassDeclaration) // ReferenceError: ClassDeclaration is not defined console.log(VarClass) // class {} console.log(LetClass) // ReferenceError: letClass is not defined
class 类完全可以看成构造函数的另一种写法,这种写法可以让对象的原型属性和函数更加清晰。
javascript
复制代码
class Person {} console.log(typeof Person) // function console.log(Person===Person.prototype.constructor) // true
上面代码表明,类的数据类型就是函数,类本身就指向构造函数。
constructor 方法是一个特殊的方法,这种方法用于创建和初始化一个由class创建的对象。通过 new 关键字生成对象实例时,自动会调用该方法。一个类只能拥有一个名为constructor构造函数,不能出现多个,如果定义了多个constructor构造函数,则将抛出 一个SyntaxError错误。如果没有定义constructor构造函数,class 会默认添加一个空的constructor构造函数。
kotlin
复制代码
class Person {} // 等于 class Person { constructor () {} }
使用new操作符实例化Person的操作等于使用new调用其构造函数。唯一可感知的不同之处就是,JavaScript解释器知道使用new和类意味着应该使用constructor函数进行实例化。
类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。
scss
复制代码
class Person {} Person() // TypeError: Class constructor Test1 cannot be invoked without 'new'
使用new调用类的构造函数会执行如下操作。
一起来看看下面例子:
javascript
复制代码
class Person {} class Test1 { constructor () { console.log('Test1 初始化') } } class Test2 { constructor () { this.test='通过初始化构造函数设置值' } } // 构造函数返回指定对象 const dataObj={ n: '自定义实例对象' } class Test3 { constructor () { this.test='通过初始化构造函数设置值' return dataObj } } const a=new Person(); const b=new Test1(); // Test1 初始化 const c=new Test2(); console.log(c.test) // 通过初始化构造函数设置值 const d=new Test3(); d instanceof Test3; // false console.log(d) // { n: '自定义实例对象' }
类实例化时传入的参数会用作构造函数的参数。如果不需要参数,则类名后面的括号也是可选的:
javascript
复制代码
class Person { constructor (...args) { console.log(args.length) } } class Test1 { constructor (test) { console.log(arguments.length) this.test=test || '默认值' } } // 不传值 可以省略() const a=new Person // 0 const b=new Person('1', '2') // 2 const c=new Test1() // 0 console.log(c.test) // 默认值 const d=new Test1('传入值') // 1 console.log(d.test) // 传入值 const d=new Test1('1', '2', '3') // 3 console.log(d.test) // 1
与立即调用函数表达式相似,类也可以立即实例化:
arduino
复制代码
const a=new class Person { constructor (text) { this.text=text console.log(text) } }('立即实例化类'); // 立即实例化类 console.log(a); // Person
类的语法可以非常方便地定义应该存在于实例上的成员、应该存在于原型上的成员,以及应该存在于类本身的成员。
实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上。
每个实例都对应一个唯一的成员对象,这意味着所有成员都不会在原型上共享:
javascript
复制代码
class Person { constructor (x, y) { this.text=new Number(1); this.x=x this.y=y this.getText=()=> {console.log(this.text)} } toString () { console.log(`${this.x}, ${this.y}`) } } const test1=new Person('x', 'y'), test2=new Person('x2', 'y2'); console.log(test1.getText()) // Number {1} console.log(test2.getText()) // Number {1} console.log(test1.x, test1.y) // x y console.log(test2.x, test2.y) // x2 y2 // console.log(test1.text===test2.text) // false // console.log(test1.getText===test2.getText) // false test1.text='测试' console.log(test1.getText()) // 测试 console.log(test2.getText()) // Number {1} test1.toString() // x, y test2.toString() // x2, y2 test1.hasOwnProperty('x'); // true test1.hasOwnProperty('y'); // true test1.hasOwnProperty('getText'); // true test1.hasOwnProperty('toString'); // false test1.__proto__.hasOwnProperty('toString'); // true // 类的实例共享同一个原型对象 console.log(test1.__proto__===test2.__proto__) // true // 也可以使用ES6提供的 Object.getPrototypeOf 来获取prototype const test1Prototype=Object.getPrototypeOf(test1) test1Prototype.myName='共享字段' // test2 中也是能获取到 console.log(test2.myName) // 共享字段
x、y、text和getText都是实例对象test1自身的属性,所以hasOwnProperty()方法返回true,而toString()是原型对象的属性(因为定义在Person类),所以hasOwnProperty()方法返回false,这些都与 ES5 的行为保持一致。
类的所有实例共享同一个原型对象。这也意味着,可以通过实例的__proto__属性或Object.getPrototypeOf方法获取原型为“类”添加方法,这将会出现共享情况,必须相当谨慎,不推荐使用,因为这会改变“类”的原始定义,影响到所有实例。
类方法等同于对象属性,因此可以使用字符串、符号或计算的值作为键:
scss
复制代码
const symbolKey=Symbol('test') class Person { stringKey () { console.log('stringKey') } [symbolKey] () { console.log('symbolKey') } ['calculation' + '1'] () { console.log('calculation') } } const a=new Person(); a.stringKey() // stringKey a[symbolKey]() // symbolKey a.calculation1() // calculation
在 class 内部可以使用 get 与 set 关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
kotlin
复制代码
class Person { constructor (test) { this.test=test || '默认值' } get prop () { return this.test } set prop (value) { console.log(`setter prop value: ${value}`) this.test=value } } const p=new Person('1') p.prop // 1 p.prop='2' // setter prop value: 2 p.prop // 2
set函数和get函数是设置在属性的 Descriptor 对象上的,可以通过 Object.getOwnPrototyDescriptor 来获取指定属性的指定描述对象。
javascript
复制代码
const descriptor=Object.getOwnPropertyDescriptor(Person.prototype, 'prop') 'get' in descriptor // true 'set' in descriptor // true
如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数:
scss
复制代码
class Person { constructor(...args) { this.args=args; } * generatorFun () { for (let arg of this.args) { yield arg; } } } const a=new Person(1,2,3,4); const generatorNext=a.generatorFun().next generatorNext() // {value: 1, done: false} generatorNext() // {value: 2, done: false} generatorNext() // {value: 3, done: false} generatorNext() // {value: 4, done: false} generatorNext() // {value: undefined, done: true}
类的方法内部如果含有this,它默认指向类的实例。但是某些情况是指向当前执行环境;
arduino
复制代码
class Person { constructor () { this.text='1' } getText () { console.log(this.text) } } const a=new Person() a.getText() // 1 const {getText}=a // this 指向为undefined class 默认严格模式 getText() // TypeError: Cannot read properties of undefined (reading 'text')
上面找不到 this 问题,this会指向该方法运行时所在的环境,因为 class 内部是严格模式,所以 this 实际指向的是undefined。有两个方法解决当前问题:
第一、构造方法中绑定this:
kotlin
复制代码
class Person { constructor() { this.text='1' this.getText=this.getText.bind(this) } getText () { console.log(this.text) } }
第二、使用箭头函数:
javascript
复制代码
class Person { constructor() { this.text='1' } getText=()=> { console.log(this.text) } }
箭头函数内部的 this总是指向定义时所在的对象。
第三、使用proxy 在获取方法的时候自动绑定this:
kotlin
复制代码
function classProxy (target) { const map=new Map() // 读取拦截配置, 只需要配置 get const hanlder={ get(target, key) { const val=Reflect.get(target, key) // 要获取的是函数执行, 如果不是函数就直接返回 val if (typeof val !=='function') return val if (!map.has(val)) { // 使用 bind改变运行函数的 this为拦截的实例对象 map.set(val, val.bind(target)) } return map.get(val) } } const proxy=new Proxy(target, hanlder) return proxy } class Person { constructor (text) { this.text=text } getText () { console.log(this.text) return this.text } } const person=classProxy(new Person('test')) const { getText }=person getText() // test
静态方法、静态属性及静态代码块(proposal-class-static-block)都是使用 static关键字定义的属性、方法或块只能 class 自己用,不能通过实例继承。
静态方法中的this 指向的是 当前类,而不是指向实例对象。静态属性是当前类自身的属性。
javascript
复制代码
class Person { static staticProp='Person静态属性' constructor () { // 通过 类名 获取 console.log(`output: ${Person.staticProp}`) // 也可以通过 构造函数的属性 this.constructor.staticFun1() } static staticFun1 () { this.staticFun2() console.log(`output: 静态方法staticFun1,获取Person静态属性==> ${Person.staticProp}`) } static staticFun2 () { console.log(`output: 静态方法staticFun2,获取静态属性==> ${this.staticProp}`) } } Person.staticProp // 静态属性 Person.staticFun1() // output: 静态方法staticFun2,获取静态属性 Person静态属性 // output: 静态方法staticFun1,获取Person静态属性==> Person静态属性 const a=new Person() // output: Person静态属性 a.staticProp // undefined a.staticFun1 // undefined a.staticFun2 // undefined // 通过其原型构造函数还是能获取到 这些静态属性及方法 不推荐使用 // a.__proto__.constructor.staticProp // a.__proto__.constructor.staticFun1()
是在 Class 内创建了一个块状作用域,这个作用域内拥有访问 Class 内部私有变量的特权,在这个代码块内部,可以通过 this 访问 Class 所有成员变量,包括 # 私有变量,且这个块状作用域仅在引擎调用时初始化执行一次 ,决解以前初始化静态类属性需要设置一个静态变量初始化逻辑。
注意: static 变量或代码块都按顺序执行,父类优先执行,一个类中允许多个静态代码块存在。
kotlin
复制代码
class Person { static staticProp='静态属性' static staticPropArr=[] static staticPropObj={} static getStatic (name) { console.log(`获取:${name}`, name && this[name]) return name && this[name] } static resetData (name, data) { name && (this[name]=data) console.log(`重置:${name}`, name && this[name]) } static { console.log('静态代码块执行'); this.getStatic('staticProp'); this.getStatic('staticPropArr'); this.getStatic('staticPropObj'); this.resetData('staticProp', '重置静态属性'); this.resetData('staticPropArr', ['重置静态数组']); this.resetData('staticPropObj', { text: '重置静态对象' }); this.staticPropObj.staticBlock1='代码块中直接设置' console.log(this.staticPropObj) } } /** * 静态代码块执行 获取:staticProp 静态属性 获取:staticPropArr [] 获取:staticPropObj {} 重置:staticProp 重置静态属性 重置:staticPropArr ['重置静态数组'] 重置:staticPropObj {text: '重置静态对象'} {text: '重置静态对象', staticBlock1: '代码块中直接设置'} */
上面代码中可以看出,static 关键字后面不跟变量,而是直接跟一个代码块,就是 class static block 语法的特征,在这个代码块内部,可以通过 this 访问 Class 所有成员变量,包括 # 私有变量。
在这里提前使用一下私有变量,理论上 class 私有变量外部是访问不了的,但是有了静态代码块( *class-static-block *)之后,我们可以将私有属性暴露给外部变量:
javascript
复制代码
let privateValue export class Person { #value constructor(x) { this.#value=x } static { privateValue=(obj)=> obj.#x; } } export function getPrivateValue (obj) { return privateValue(obj) } // 在另一个文件中 import { Person, getPrivateValue } from 'xxx' const a=new Person('私有变量') getPrivateValue(a) // 私有变量
其实class-static-block本质上并没有增加新功能,我们完全可以用普通静态变量代替,只是写起来很不自然,所以这个特性可以理解为对缺陷的补充,或者是语法完善,个人认为现在越来越像java。
私有属性和私有方法,是只能在类的内部访问的方法和属性,外部不能访问,不可以直接通过 Class 实例来引用,其定义方式只需要在方法或属性前面添加#。
私有属性:
arduino
复制代码
class Person { #privateVar1; #privateVar2='默认值'; constructor (text) { this.#privateVar1=text || '--' console.log(this.#privateVar1) } getPrivateData1 (key) { // 这里是获取不了的 console.log('传入key来获取私有变量:', this[key]) console.log('获取私有变量', this.#privateVar2, this.#privateVar1) } static staticGetPrivateData (person, key) { console.log('静态方法获取私有变量:', person.#privateVar2, person.#privateVar1) // 下面是获取不到 console.log('静态方法传入key来获取私有变量:', person[key]) } } const a=new Person() // 不传 默认 -- // output: -- a.getPrivateData1('#privateVar1') // output: 传入key来获取私有变量:undefined // output: 获取私有变量: 默认值 -- // 使用静态方法 Person.staticGetPrivateData(a, '#privateVar1') // output: 静态方法获取私有变量: 默认值 -- // output: 静态方法传入key来获取私有变量:undefined
从上面代码中我们可以看到,私有变量是只能内部读取或写入,不能通过动态key读取(外部调用就会报错)
注意:在class 中 公共属性 test 与 #test 是两个完全不同的值;
私有方法:
arduino
复制代码
class Person { #private; constructor () { this.#private='私有变量' this.#methods() // 调用私有方法 } #methods () { console.log('私有方法#methods:', this.#private) } static #staticMethods (person) { if (person) { console.log('静态私有方法#staticMethods person获取值', person.#private) person.#methods() } } init1 () { this.#methods() console.log('使用this') Person.#staticMethods(this) } init2 (person) { if (person) { console.log('使用传入实例') Person.#staticMethods(person) } } } const a=new Person() // output: 私有方法#methods: 私有变量 // a.#methods() SyntaxError // a['#methods']() TypeError: a.#methods is not a function a.init1() // output: 私有方法#methods: 私有变量 // output: 使用this // output: 静态私有方法#staticMethods person获取值 私有变量 // output: 私有方法#methods: 私有变量 a.init2(a) // output: 使用传入实例 // output: 静态私有方法#staticMethods person获取值 私有变量 // output: 私有方法#methods: 私有变量
从上面代码中我们可以看到,私有方法只能内部调用,在外部调用就会报错。
使用 extends 关键字,让子类继承父类的属性和方法。
javascript
复制代码
class Person { num=1 text='person' getNum=()=> console.log(this.num, this) addNum () { console.log(++this.num, this) } } // 继承 class Child extends Person { constructor () { super() this.getText() } getText=()=> console.log(this.text, this) } const a=new Child() // output: person Child {} console.log(a instanceof Child) // output: true console.log(a instanceof Person) // output: true a.getText() // output: person Child {} a.getNum() // output: 1 Child {} a.addNum() // output: 2 Child {} a.getNum() // output: 2 Child {} a.text // person a.num // 2
从上面代码中,我们可以看出Child 类 继承了 Person 的属性及方法,在Child 中也是可以调用Person的方法及属性,注意 this 的值会反映调用相应方法的实例或者类。子类中(Child)如果设置了 constructor 方法 就必须调用 super() ,否则就会出现新建实例时报错,如果没有 constructor 构造函数,在实例化继承类时会调用 super() ,而且会传入所有传给继承类的参数(后面会详细讲解)。
arduino
复制代码
class Person { static staticText='staticText'; #private='private' static staticMethods1 (person) { console.log('staticMethods1', this) person.#privateMethods() } #privateMethods () { console.log('#privateMethods', this) } } // 使用表达式格式 也是可以使用 extends 继承 const Child=class extends Person { methods () { console.log('methods', Child.staticText) } } const a=new Child() a.methods() // output: methods staticText Child.staticMethods1(a) // output: staticMethods1 class Child {} // output: #privateMethods Child {} Person.staticMethods1(a) // output: staticMethods1 class Person {} // output: #privateMethods Child {}
使用表达式格式 也是可以使用 extends 继承,类 的静态方法与属性是可以继承的,其私有属性及方法是不能继承的,可以从继承的共有方法与静态方法 中获取其私有属性或调用其私有方法。
super 关键字可以作函数使用,也可以作对象使用,但是其只能在继承类中使用,且只能在继承类的constructor 构造函数、实例方法和静态方法中使用。作为函数时是在 继承类的constructor 构造函数中使用,根据要求如果继承类中定义了constructor构造函数就必须要调用super方法(调用父类的constructor),否则就会报错。
scala
复制代码
class Person {} class Child extends Person { constructor () { // 如果不调用 super() 就会报错 // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor super() // 调用父级的constructor console.log(this) // Child {} } }
注意: constructor() 中必须super() 顶部首段执行代码,否则也是一样报错;
在使用 super() 时应该注意下面几个问题:
作者:前端农民晨曦
链接:https://juejin.cn/post/7098891689955164168
JS中有类?我们都知道JavaScript是基于原型的语言,并没有内置的类概念。但从ES6开始,JavaScript引入了class关键字作为语法糖,它提供了一种更简洁、更类似于传统面向对象编程的语法来创建对象。
class关键字在语法上类似于许多其他基于类的语言,如Java或C++,但JavaScript的类仍然是基于原型的。
class语法糖有哪些基本用法?
在JavaScript中,你可以使用class关键字来定义一个类。类是一个抽象的概念,它描述了具有相同属性和方法的对象的集合。在类中一定有一个构造器函数constructor,我们可以在constructor中接收参数,将我们需要的参数都加进去。
class Person {
constructor(name, age) {
this.name=name;
this.age=age
}
sayHello() {
console.log(`Hello, my name is ${this.name} and my age is ${this.age}`);
}
}
在上面的例子中,我们定义了一个名为MyClass的类,它有一个构造函数(constructor)和一个方法(sayHello)。构造函数在创建类的实例时被调用,它接受一个参数name并将其存储在实例上。sayHello方法用于打印一条问候信息。
创建类的实例就是将类实例化,我们使用new关键字调用类的构造函数来创建一个新的对象,这个对象就是该类的实例。
这个新创建的对象会继承类的所有属性和方法(除非它们是静态的),并且可以有自己的状态(即实例属性和方法)。这里以我自己为例
class Person {
constructor(name, age) {
this.name=name;
this.age=age
}
sayHello() {
console.log(`Hello, my name is ${this.name} and my age is ${this.age}`);
}
}
let p=new Person('绵绵冰', 18);//调用然后传入参数
console.log(p);
p.sayHello();
在上面的例子中,我在class类里面传入了参数,定义了一个sayHello方法;我将这个类实例化为p,这个p可以访问我在类中定义的方法
类的实例对象可以访问类中的方法,就像原型一样
为什么类能实现这样的功能呢?当然和原型离不开关系,因为我们都知道JS是基于原型的语言。js的类也是基于原型的。
他的运行机制主要有三步:
因为当你定义一个类时,实际上你定义了一个构造函数(通过class关键字),并且在这个构造函数上挂载了一些属性和方法。这些属性和方法实际上是定义在构造函数的prototype对象上的。
当你使用new关键字创建类的实例时,JavaScript引擎会执行以下步骤:
由于实例的__proto__指向了类的prototype对象,所以当你尝试访问实例上的一个属性或方法时,JavaScript会首先在实例自身上查找。如果找不到,它会继续在实例的原型链上查找,直到找到为止,或者直到原型链的末尾(即Object.prototype)
既然JS中的类也是基于原型的,那么就一定和原型一样有类似的继承方法。
在JS中,我们可以使用extends关键字来让一个类继承另一个类的属性和方法。这允许你创建更复杂的类结构,并实现代码的重用。
class Person {
constructor(name, age) {
this.name=name;
this.age=age
}
sayHello() {
console.log(`Hello, my name is ${this.name} and my age is ${this.age}`);
}
}
class Person2 extends Person {
constructor(name, age) {
super(name); // 调用父类的构造函数
this.age=age;
}
introduce() {
console.log(`Hi, I'm ${this.name} and I'm ${this.age} years old.`);
}
}
const p2=new Person2('绵绵冰2', 25);
p2.sayHello();
p2.introduce();
在上面的例子中,p2就继承了p的class里面的属性和方法,并且添加了新的方法introduce来介绍自己。
在JavaScript中,我们可以使用static关键字来定义静态方法。静态方法是挂载在类上的方法,而不是类的实例上。这意味着我我们可以直接通过类名来调用静态方法,而不需要创建类的实例。
class greet {
static greet() {
console.log('Hello from a static method!');
}
}
greet.greet(); // 输出: Hello from a static method!
在这个例子中,我们定义了一个名为greet的类,并在其上定义了一个静态方法greet。我们直接通过类名调用了这个方法,而无需创建类的实例。
这是一种特殊类型的方法,用于读取和写入对象的属性。访问器由getter和setter方法组成。getter方法是一个没有参数的方法,用于返回与对象属性相关联的值。setter方法则接收一个参数(即要写入属性的值),并用于修改与对象属性相关联的值。
class Person {
constructor(name, age) {
this._name=name;
this._age=age;
}
// getter方法,用于读取_name属性
get name() {
return this._name;
}
get age() {
return this._age;
}
// setter方法,用于设置_name属性
set age(newAge) {
this._age=newAge;
}
set name(newName) {
this._name=newName;
}
}
const person=new Person('绵绵冰', 18);
// 使用getter方法读取属性
console.log(person.name);
console.log(person.age);
// 使用setter方法设置属性
person.name='绵绵冰2号';
person.age=20;
// 再次使用getter方法读取属性,验证setter方法是否生效
console.log(person.name);
console.log(person.age);
在上面这个简单的例子中,我们通过get方法拿到了name和age并且将他们转化为了私有属性(使用下划线前缀表示私有属性,尽管在JavaScript中并不是真正的私有),再通过set将这些属性设置为新的属性值。但是这个简单的例子存在一些问题,我们需要对传入的参数进行一些判断
class Person {
constructor(name, age) {
this._name=name;
this._age=age;
}
get name() {
return this._name;
}
get age() {
return this._age;
}
set name(newName) {
// 在这里添加一些验证逻辑
if (typeof newName==='string' && newName.trim() !=='') {
this._name=newName;
} else {
console.error('无效');
}
}
set age(newAge) {
if (typeof newAge==='number' && newAge >=0) {
this._age=newAge;
} else {
console.error('无效');
}
}
}
const person=new Person('绵绵冰', 18);
console.log(person.name);
console.log(person.age);
person.name='绵绵冰2';
person.age=20;
console.log(person.name);
console.log(person.age);
// 尝试设置一个无效的值,查看setter方法中的验证逻辑是否生效
person.age='十八'; // 这将触发setter方法中的console.error
原文转自:https://juejin.cn/post/7382022102679207955
*请认真填写需求信息,我们会在24小时内与您取得联系。