JavaScript学习笔记

这篇文章是JavaScript的学习笔记。内容主要来自MDN的JavaScript Guide - JavaScript | MDN

语法和类型

基础

JS采用Unicode字符集,并且大小写敏感。语句使用;结束。一行单条语句的;不是必需的。建议始终加上;

注释

注释与C++类似,有单行注释//,和多行注释/* */。多行注释不能嵌套。

声明

有四种方式方式定义变量:

  • 直接赋值:定义一个全局变量(不严格模式)
  • var:定义一个变量(全局或局部作用域)
  • let:定义一个局部变量(块作用域)
  • const:定义一个常量

形式均类似var name1 = value1 [, name2 = value2 [, ... [, nameN = valueN]]];。合法的标识符由英文字母、_$开头,后还可跟数字组成。

未初始化的变量值为undefined。使用未声明的变量会产生ReferenceError

undefined转换成bool类型为false,转化成数字类型为NaNnull转换成bool类型为false,转化成数字类型为0。可以使用严格等号===判断一个变量是否为undefinednull

(var)函数外声明的变量为全局变量,函数内部声明的变量为局部变量。ECMA2015中出现了块作用域(let),之前没有。JS函数可嵌套。

var声明的变量会被提前(相当与声明提前,但初始化并未提前)。letconst不会被提前。注意:函数声明也会被提前,也就是函数可以在声明前使用。

全局变量实际上是全局对象的属性,对于web而言,就是window

const变量不能和同一作用域的其他函数和变量重名。const对象的属性不受保护,可重新赋值。

数据结构和类型

共有8种数据类型:

  • 7种基础数据类型
    • Boolean:布尔
    • null
    • undefined
    • Number:数字
    • BigInt:高精度整数
    • String:字符串
    • Symbol:符号 (ECMA2015)
  • 对象

JavaScript是动态类型语言。

字符串类型与数字类型进行+运算,数字类型转换为字符串类型。而其他的运算,字符串类型转换为数字类型。注意:可以通过一元+,将字符串转换为数字。

有以下两个函数可以进行字符串到数字的转换:

  • parseInt(string, radix):转换为整数
  • parseFloat(value):转换为浮点数

字面常量

数组字面常量是由[]括住的,分割的列表。未指定的元素将为undefined。如果列表的最后有尾随的逗号,将被忽略。建议省略的元素显式地用undefined表示。

truefalse是两种布尔类型的字面常量。不要混淆基础的布尔类型和布尔对象。

整数字面常量有以下几种:

  • 10进制:非0开头的数字序列
  • 8进制:0开头,或0o(0O)开头
  • 16进制:0x(0X)开头
  • 2进制:0b(0B)开头

浮点字面常量与其他语言类似,一个浮点常量必须有一个数字和小数点或指数部分。

对象字面常量是由{}括住的:,分割的键值对列表。不应在语句的开始处使用对象字面常量。键可采用标识符、字符串(包括空字符串)和数字。访问属性可采用.(必须是合法的标识符)或[]运算符(必须是值,如字符串)。

ES2015支持构造对象时指定原型(__proto__: theProtoObj)、简写foo: foo语句成foo、直接定义成员函数name() {}、调用基类super.id和运行时计算属性名[expr]: value

正则表达式字面常量是由//括住的正则表达式。

字符串字面常量是由''""括住的字符串。可调用字符串对象拥有的属性,如length。在ES2015中,还支持了模板字符串,`...${var}...`,可跨多行。此外还可以使用标签自定义字符串。字符串转义规则同其他语言。

执行控制和错误处理

块语句

多条语句用花括号括起来构成块语句。经常被用于控制语句的语句体部分。

条件语句

if-else语句、switch语句与其他语言类似。

不建议直接在条件判断处使用赋值语句,如果确实需要,则加上括号,如if ((x = y)) {}

假值包括:

  • false
  • undefined
  • null
  • 0
  • NaN
  • ""(空字符串)

注意:new Boolean(false)的值为真。

异常处理语句

throw语句与其他语言相似,可以throw各种值。catch语句形式为catch (catchID) { statements },其中的catchID作用域仅在catch块中。finally语句与Java类似。注意:catch中的return语句和异常抛出语句会被挂起,先执行finally中的语句,如果finally语句中存在return语句,则之前的返回值和抛出的异常不再起作用。

可以使用Error对象用于异常处理,它有namemessage两个属性。

可以使用Promise用于异步或者延迟操作的控制处理。

循环

for、while、do…while、beak、continue语句与其他语言相似。break、continue语句可以带有label。

for…in语句遍历一个对象的所有属性。for…of语句遍历可迭代对象(ArrayMapSetArguments等等)。

函数

定义和调用函数

函数定义(又称函数声明、函数语句)形如function name(parameterlist) { statements },其中parameterlist是逗号分隔的参数列表。return语句可以返回值。数据类型传参采用传值,对对象(包括Array)属性的修改会对外部可见。

函数表达式与函数声明形式类似,但作为语句的一个部分,其中name可选。也可以通过Function构造函数从字符串里构造出函数。函数表达式不会提前。

方法是一种作为对象属性的函数。

ES6支持默认形参,以及...name的方式声明剩余形参,其中name是存放额外参数的数组。

注意:函数声明会被提前。函数表达式赋值的对象会被提前,但其值为undefined

函数调用语法和其他语言一样,可以递归。函数本身也是对象,可以通过apply方法或call方法调用。

作用域和函数栈

在函数内,可以访问函数内定义的变量(及函数)以及父函数所能访问到的变量(及函数),但不能访问到子函数的变量(及函数)。

在函数内部,可以通过arguments.callee引用自己,也可以通过arguments[index]arguments.length获取参数及其个数。注意:arguments不是数组。函数调用提供的参数和函数声明提供的参数数目不一致,提供少了的参数是默认参数或(没默认参数)undefined,提供多了的参数可以通过arguments访问。

嵌套函数和闭包

嵌套函数会形成闭包,闭包内引用的外层作用域的变量会被保留,直到该闭包销毁。闭包按引用捕获外层变量。

注意:内层作用域会屏蔽外层作用域的同名变量。闭包不捕获this,闭包中的this变量为其调用者而非创建者。对于普通函数this是(构造函数)一个全新的对象或(strict模式的函数调用)undefined或(以对象方法的形式调用)原始对象。

箭头函数

语法形式如(parameters, ...) => { statements }或者(parameters, ...) => expression创建一个箭头函数。当形参个数为1个时,可省略括号,支持默认形参和剩余形参。箭头函数没有自己的thisargumentssupernew.target

注意:箭头函数捕获this,箭头函数中的this变量为其创建者而非调用者。

预定义函数

  • eval:执行字符串中的JS代码;
  • isFiniteisNan:判断是否是有限数字和NaN;
  • parseFloatparseInt:解析字符串返回数字,parseInt还可以选择基数;
  • encodeURIdecodeURI:将URI中的某些字符(不改变URI本身的地址)转换为转义字符,和转换回去;
  • encodeURIComponentdecodeURIComponent:将整个URI的某些字符转换为转义字符,和转换回去。

运算符

与其他类C语言类似。

赋值支持解包的语法:var [var1, var2, ...] = array

相等判断有三类:

  • ==&!=:不严格相等判断,进行必要的类型转换后再判断;
  • ===&!==:严格相等判断,不进行类型转换(类型不符则一定不等);
  • Object.is(a, b):与严格相等的不同之处在于,Object.is(-0, +0)为false,而Object.is(NaN, NaN)为true。

对于数学运算,除以0会产生Infinity。ES7支持**作为乘方运算符。

对于位运算符,会将操作数转化为32为整型,高位丢弃。右移运算符分为符号右移>>和补0右移>>>

对于逻辑运算符,expr1 && expr2expr1能转换为false时返回expr1,否则返回expr2expr1 || expr2expr1能转换为true时返回expr1,否则返回expr2

使用+可以连接字符串。

delete表达式可以有以下几种形式:

  • delete objectName:只能针对隐式声明的变量(不采用var,而是直接赋值);
  • delete objectName.property:删除属性;
  • delete objectName[index]:删除元素,不影响数组长度;
  • delete property:只能在with语句中用。

delete表达式会返回布尔值,操作可行返回true,不可行返回false。用var定义的变量和内置属性都无法删除。

typeof operand或者typeof (operand)返回operand类型对应的字符串。typeof null返回"object"。对于对象,一律返回"object";对于函数和方法。一律返回"function"

void (expression)或者void expression会对expression进行求值,却不返回结果(也就是返回undefined)。

对于关系运算符,propNameOrNumber in objectName返回对象是否存在某属性,可用于数组和对象。objectName instanceof objectType返回对象是否为某类型的实例。

表达式

JavaScript包含以下5种表达式:

  • 数学
  • 字符串
  • 逻辑
  • 初等
  • 左值

初等表达式

this关键字指当前的对象。()用于调整优先级。此外,非标准的JS还支持两种推导式语法:数组推导式[for (x of y) x]和生成器推导式(for (x of y) x)

左值表达式

new可以创建对象的实例。super可以调用父对象的函数。通过展开运算符...array可以将数组展开至字面数组或者函数参数处。

数字和日期

数字

在JavaScript中,所有的数字都是64位浮点数。

Number对象有如下的属性和方法。

  • MAX_VALUEMIN_VALUE:最大和最小的数字;
  • NANNEGATIVE_INFINITYPOSITIVE_INFINITY:NaN,正无穷和负无穷;
  • EPSILON:1和比1大的最小数之间的差;
  • MIN_SAFE_INTEGERMAX_SAFE_INTEGER:最小和最大的安全整数,分别是($-2^{53}+1$和$+2^{53}-1$);
  • parseFloat()parseInt():和全局的一样;
  • isFinite()isNaN():和全局的类似,但是不会将参数转换为数字;
  • isInteger():返回是否是整数;
  • isSafeInteger():返回是否是安全整数。

Number.prototype有如下方法:

  • toExponential():返回指数形式的字符串;
  • toFixed():返回浮点形式的字符串;
  • toPrecision():返回指定精度的浮点形式的字符串。

Math对象

Math对象包含了如PIE之类的常量,还有各种数学函数。

Date对象

Date对象的范围是-100,000,000天到100,000,000天相较于1970年1月1日UTC时间。

通过new Date()创建Date对象,参数可以是

  • 无:现在的时间戳;
  • 代表时间戳的字符串:形如Month day, year hours:minutes:seconds,可以省略小时、分钟或秒。
  • 代表年(FullYear)、月、日的整数
  • 代表年(FullYear)、月、日、小时、分钟、秒的整数

注意:月份从0计数。年份(非FullYear)是从1900开始的年数。

Date对象的方法大致有如下几类:

  • set方法
  • get方法
  • to方法:返回字符串
  • 解析UTC时间

文字处理

字符串

JavaScript的字符串采用UTF-16编码。

对于字符串字面常量,通过\x可以转义16进制字符;通过\u后跟4个16进制字符可以转义UTF-16字符;在ES6中可以通过\u{xxxx}转义Unicode字符。通过String.fromCodePoint()可以将Unicode编码的数字转成字符,通过String.prototype.codePointAt()可以返回指定位置的Unicode字符。

String对象是对String基础数据类型的一层封装。对于字符串字面常量的成员函数调用,会创建临时的String对象完成。注意:应尽量使用String基础数据类型而非String对象。

String对象只有一个属性length。String对象和基础数据类型都是不可变的,对元素的赋值是无效的。

String有如下方法:

  • charAtcharCodeAtcodePointAt:返回指定位置的字符或编码;
  • indexOflastIndexOf:返回子串的位置;
  • startsWithendsWithincludes:返回字符串是否以子串开头、结尾或包含子串;
  • concat:连接字符串;
  • fromCharCodefromCodePoint:从编码中构建字符串;
  • split:以某字符串为分隔符分割字符串;
  • slicesubstringsubstr:取出子字符串;
  • matchmatchAllreplacesearch:使用正则表达式匹配;
  • toLowerCasetoUpperCase:大小写转换;
  • normalize:返回Unicode正规化的字符串;
  • repeat:重复字符串;
  • trim:删除前导和后继字符。

模板字符串是``括起的字符串。模板字符串可以多行(保留回车符),也可以包含占位符,形如${expression}

国际化

CollatorNumberFormatDateTimeFormat的构造函数是Intl对象的属性,被用于国际化。

正则表达式的特殊字符如下:

  • \:转义字符,注意:在字符串中还需要转义该字符;
  • ^:匹配开始,多行模式下匹配行首;
  • $:匹配末尾,多行模式下匹配行末;
  • *:出现0或多次,等价于{0,}
  • +:出现1或多次,等价于{1,}
  • ?:出现0或1次,等价于{0,1}
  • .:匹配任意字符,默认情况下换行符除外;
  • (x):捕获组,可以用\1\2等等来引用捕获组,在replace的第2个参数中,可以用$&$1$2等等来引用;
  • (?:x):不捕获的组;
  • x(?=y):匹配后面跟着yx
  • x(?!y):匹配后面不跟yx
  • (?<=y)x:匹配前面有yx
  • (?<!y)x:匹配前面没有yx
  • x|y:匹配xy
  • {n}:出现n次;
  • {n,}:出现至少n次;
  • {n,m}:出现nm次;
  • [xyz]:匹配xyz字符,特殊字符不用转义,只需要转义-^]\
  • [^xyz]:匹配除xyz之外的字符;
  • \b\B:匹配单词边界和非单词边界,对于退格使用[\b]匹配;
  • \d\D:匹配数字和非数字;
  • \s\S:匹配空白符和非空白符;
  • \w\W:匹配英文数字和下线符或不匹配它们;
  • \cX:匹配Ctrl—X
  • \f\n\r\t\v\0:匹配换页、换行、回车、制表、垂直制表和空字符;
  • \xhh\uhhhh:8进制和16进制字符,必须是2位或4位十六进制字符;
  • \u{hhhh}:只有设置了u才起效,匹配Unicode编码字符。

对于字符*/\等都需要用\转义。

有如下函数使用了正则表达式。

  • RegExp.prototype.exec:执行一次搜索,返回匹配信息(包括indexinput属性);
  • RegExp.prototype.test:测试是否匹配,返回Bool值;
  • String.prototype.match:执行一次搜索,返回匹配信息,(对于设置了g的会搜索全部);
  • String.prototype.matchAll:执行搜索,返回捕获组的迭代器(Node.js是12开始支持);
  • String.prototype.search:执行搜索,返回出现的索引,找不到返回-1;
  • String.prototype.replace:执行替换;
  • String.prototype.split:切割字符串。

正则表达式对象包含lastIndexsource属性,lastIndex只在设置了gy才启用,source是原始的正则表达式字符串。

有以下的设置选项:

  • g:全局搜索;
  • i:不区分大小写搜索;
  • m:多行搜索;
  • s:允许.匹配换行;
  • u:见上,Unicode编码;
  • y:sticky搜索(与g类似)。

正则表达式

可以使用/xxxx/或者new RegExp('xxxx')定义正则表达式。前者是在脚本加载时编译,后者是运行时编译。

容器

下标索引的容器

以下3种创建数组的方式等价,1) new Array(element0, element1, ..., elementN),2) Array(element0, element1, ..., elementN),3) [element0, element1, ..., elementN]

创建空的定长数组也有3个方式,1) new Array(arrayLength);,2) Array(arrayLength);,3) var arr = []; arr.length = arrayLength;

通过array[index] = value可以给数组赋值。如果index不是整数,则会创建一个属性。

注意:不建议使用for...in遍历数组,因为属性也会被迭代。

数组有如下方法:

  • concat:合并多个数组并将结果返回;
  • join:连接数组成字符串;
  • pushpop:在数组末尾插入或删除元素;
  • shiftunshift:在数组开始处删除和插入元素;
  • slice:抽取一段数组并返回;
  • splice:删除或替换一段数组并返回被删除的元素;
  • reverse:翻转数组;
  • sort:排序,可以接受一个比较函数;
  • indexOflastIndexOf:查找元素所在位置;
  • mapfilterforEach:遍历数组,映射、过滤或者访问元素;
  • everysome:映射后,判断是否所有值都为真或者有值为真;
  • reducereduceRight:汇总数据。

通过Array.prototype.someFunc.call(xxx)可以对类似数组的对象调用函数,如NodeListarguments

ArrayBuffer代表了一块没有类型的存储空间,即缓冲区xxxArray视图,对ArrayBuffer的包装,这里xxx可以是Int8Uint8Uint8ClampedInt16Uint16Int32Uint32Float32Float64BigInt64BigUint64

键索引的容器

相等比较时,遵行下面的规则:

  • 相等性比较类似===
  • -0+0相等;
  • NaN与自己相等(与===不同)。

Map

Map对象存储键值对。可以用for...of遍历得到[key, value],顺序是插入的顺序。有如下属性和方法:

  • set(key, value):设置键值对;
  • get(key):查询键对应的值;
  • has(key):测试键是否存在;
  • delete(key):删除键值对;
  • clear():清空Map;
  • size:大小。

WeakMap的值只能是对象,对键的持有是weak的,也就是说如果没有其他引用,这些对象会被键值对回收。

Set

Set存储值的集合,可以用for...of以插入顺序遍历元素。有如下属性和方法:

  • add(value):添加值;
  • has(value):测试值是否存在;
  • delete(value):删除值;
  • clear():清空集合;
  • size:大小。

可以通过Array.from(set)[...set]从集合中创建数组;通过new Set(array)从数组中创建集合。

WeakSetWeakMap类似。

对象

属性

JavaScript对象是属性的集合,属性包含了键值对,值可能是个函数,这就成了方法。

可以对属性赋值,访问未赋值的属性会得到undefined。访问属性可以用object.idobject[value],键不是合法标识符的属性只能通过方括号访问(包括空串)。键只能是字符串或Symbol类型,其他类型的键会被转化为字符串。

ES5开始,有3种遍历属性的方式:

  • for...in:遍历所有的可枚举属性,包括原型链;
  • Object.keys(o):返回所有属于该对象(不包括原型链)的可枚举属性的键的数组;
  • Object.getOwnPropertyNames(o):返回所有属于该对象的键(包括不可枚举属性)的数组。

构造

创建对象有如下的方式:

  • 使用对象初始化器{property1: value1, ... },这里property1等可以是标识符、数字或字符串。如果是语句开始,需要加括号以避免和复合语句混淆。同样的对象初始化器产生的对象是不等的。所有对象字面表达式产生的对象都是Object的实例;
  • 使用构造函数:通过构造函数定义对象(构造函数首字母应当大写),再通过new创建实例。定义时,使用函数,其内部可以用this指代对象,通过对this的赋值即可创建属性;
  • 使用Object.create:它接受一个对象参数,返回新的对象,新对象的原型是该对象参数。

继承

JavaScript是基于原型的面向对象语言。JavaScript的所有对象都至少继承自另一个对象。被继承的对象称为原型。继承的属性都是来自构造函数的prototype对象。this指代调用对象。通过如下面所示的代码定义继承:

function Base() {
    this.a = ...;
    this.b = ...;
}

function Derived() {
    Base.call(this);
    this.c = ...;
}

Derived.prototype = Object.create(Base.prototype);
Derived.prototype.constructor = Derived;

其中的Derived函数也可以如下写,下面的base只是一个普通的名字:

function Derived() {
    this.base = Base;
    this.base();
    this.c = ...;
}

当JavaScript遇到new操作符,它会创建一个对象,并将它的内部属性[[Prototype]]设置成构造函数的prototype属性,再将该对象作为this传递给构造函数。

当访问属性的时候,JavaScript先查看该对象是否有这个属性,有的话就采用,如果没有就查看[[Prototype]]属性的对象,如此继续下去。

如果给constructor.prototype添加属性,那么所有该构造函数的对象都会拥有这个属性。

getter和setter

对于对象字面量,可以通过如下方式构造getter和setter:

var o = {
    get b() {
        return ...;
    }
    set c(x) {
        ...
    }
};

也可以通过如下方式创建getter和setter:

Object.defineProperty(o, 'b', {
    get: function() { return ...; },
    set: function(y) { ... }
});

也可以通过Object.defineProperties定义,形式如下:

Object.defineProperties(o, {
    'b': { get: function() { return ,,,; } },
    'c': { set: function(x) { ... } }
});

使用delete可以删除非继承属性。

只有当两个对象是同一个对象,才相等。

使用Pormise

Promise状态转化图

Promise用于异步函数回调,其构造函数接受一个函数,形如(resolve, reject) => ...,如果成功则调用resolve可以传递一个值;如果失败则调用reject也可以传递一个值。

Promise拥有如下方法:

  • then(onFulfilled, onRejected):调用指定的handler,返回一个新的promise用于形成链。如果onFulfiled不是函数,则以“Identity”替代;如果onRejected不是函数,则以“Thrower”替代。如果指定的handler
    • 返回一个值,则then返回的promise以该值resolve;
    • 不返回值,则then返回的promise以undefined resolve
    • 抛出异常,则then返回的promise以该异常reject;
    • 返回一个已经resolve的promise,则then返回的promise以该promise resolve的值resolve;
    • 返回一个已经reject的promise,则then返回的promise以该promise reject的值reject;
    • 返回一个pending的promise,则then返回的promise的resolve/reject将紧随handler返回的promise的resolve/reject。
  • catch(onRejected):等价于then(null, onRejected)

ES2017加入了async/await语法糖,形如:

async function foo() {
    try {
        const result = await doSomething();
        ...
    } catch (error) {
        ...
    }
}

await后面根一个promise,如果该promise resolve了,则返回resolve的值;如果reject了,则以异常抛出的形式抛出reject的值。async函数返回一个promise。

当promise被reject时,会有以下两个消息中的一个发往全局对象(window)或者Worker(process):

  • rejectionhandled:当一个promise reject,并被处理时发出;
  • unhandledrejection:当一个promise reject,但没有handler时发出。

在这两种情况下,会有一个PromiseRejectionEvent对象作为参数,它有promisereason两个属性。

注意:Node.js与上面描述的有些许不同。

此外还有如下4个函数:

  • Promise.resolvePromise.reject:创建一个已经resolve或已经reject的Promise;
  • Promise.allPromise.race:执行数组所有的promise或竞争。

即使是已经resolve的对象,传递给then()的函数也会异步调用(过一会调用)。

迭代器和生成器

迭代器

所谓迭代器,就是有next()方法,返回一个有两个属性的对象:

  • value:序列中的下一个值,当donetrue时可省略;
  • done:最后一个值是否已经被获取(true时,迭代器位于past the end)。

可迭代对象是值实现了Symbol.iterator返回一个迭代器的对象。

StringArrayTypedArrayMapSet都是可迭代对象。

生成器

生成器形如下面的代码,函数内可以yield多次:

function* foo() {
    ...
    yield value;
    // or
    yield* iterable;
    ...
}

它会返回一个生成器,它是可迭代对象,每个生成器只能遍历一次。实际上生成器的Symbol.iterator方法返回了它自己。

next()方法还接受一个值,这个值通过result = yield value得到。调用throw()方法还可以在生成器中抛出异常。

元编程

Proxy

在ES6中,Proxy对象可以可以拦截某些操作,实现自定义行为。

使用方法为var newObject = new Proxy(oldObject, handler);,这里handler为一个对象,其方法就是trap(陷阱)。trap的行为必须遵守invariants。关于trap、它所拦截的对象和invariants见Meta programming - JavaScript | MDN

使用Proxy.revocable可以创建一个可撤销的Proxy。它返回一个对象,其proxy属性是代理的对象,其revoke()方法可以撤销代理。

Reflection

Reflect主要是将对象的操作变成函数,并与Proxy对象的方法一一对应。包含如下的函数:

  • Reflect.has():检查是否有属性;
  • Reflect.apply():将this和参数列表应用到函数;
  • Reflect.defineProperty():与Object.defineProperty()不同,如果失败不抛出异常,而是返回false

模块

命名导出:可以在定义变量、函数、类的时候用export导出,如export const foo = 42;。也可以在文件最后用花括号扩住、逗号分隔的列表导出,如export { foo, bar }

使用import { foo, bar } from 'path'即可导入,其中path是相对或绝对(即相对于站点根目录)路径。

使用了模块导入导出的主模块,需要按照如下方式导入HTML:

<script type="module" src="path"></script>

注意,模块和标准脚本是不同的,不添加type="module"会使importexport语句报错。此外还有如下不同:

  • 在本地加载模块会遇到CORS错误,而标准脚本不会;
  • 模块默认是strict模式;
  • 默认为defer的;
  • 从主模块导入的模块对外部是不可见的。

默认导出:通过export default expression;可以默认导出。再通过import name from 'path';或者import {default as name} from 'path';导入。

exportimport都可以带别名,如export { foo as bar };import { bar as foo } from 'path';

还可以创建模块对象,形如import * as Module from 'path';

还有聚合模块,用以将多个模块聚合在一起。有这样的语法:export * from 'path';export { name } from 'path';

使用import()函数可以动态加载模块,它返回Promise。

编辑本页

孙子平
孙子平
静态分析方向研究生
下一页
上一页

相关