JavaScript中的类型、对象、原型、类
以及面向对象编程深度解析
1. Javascript 基本类型(即数据类型)
我在之前很多博文中已经提到,Javascript不像Java、Python等语言“一切皆对象”。在Javascript中并非
一切皆对象,基本类型如boolean
、number
、string
、undefined
、null
、就不是对象,它们只表示类型与对象也没什么关系。在后文你会看到一些相似的东西,如Boolean
、Number
、Sstring
、Object
等,它们也表示类型,属于"引用类型"。这些引用类型们却是"对象",称之为"内置对象"或"内置函数"。JavaScript中的简单基本类型如:
简单基本类型 | 备注 |
boolean |
布尔值类型,表示两个值:“true” 或 “false” |
number |
数值类型 |
string |
字符串值类型 |
undefined |
未定义类型 |
null |
空类型,表示一个空对象的指针但本身不是对象 |
symbol |
表示独一无二的值(ES6新增) |
bigint |
表示大于 2 53 − 1 2^{53} - 1253−1 的整数(ES7新增) |
object |
表示非原始类型,也就是除number ,string ,boolean ,symbol ,bigint , null 或undefined 之外的类型。 |
需要指出,在JavaScript中使用小写字母开头表示简单基本类型,这些简单基本类型除了object都不是对象类型,而仅仅表示一系列具有某种特征的值的类型。比如:
let a = "Hello World!"
该语句的右侧"Hello World!
"的类型就是简单基本类型string
,这不是对象,而是一个不可变的值,或者称“字面量”。因此变量a的类型为string
:
typeof a
Out[]:
string
有一个具有迷惑性的事情是,既然string
类型只是一个值类型(字面量),那么就不应该具有对象所具有的属性啊。这时不可思议的事情发生了:
a.length
Out[]:
12
JavaScript语言中,在一些特定情况下,比如使用相应的属性时,值类型量会自动转换为对应的内置类型
以使用相应属性。
也就是说,虽然a的类型为string
,但执行“a.length”时,将产生一个对应的为内置类型String
的变量代替它执行之,这也不会改变a本身的类型。这是作者在Node.js环境中运行的结果:
再举一个例子,number
与Number
。
let a = 10; let b = Number(10); let c = new Number(10);
在上面的JavaScript语句中,变量a、b中存储的都是number类型的值,这些number类型的值都不是对象。变量c中存储的类似于如Java等其它语言中类的实例对象,但实际上在JavaScript中,c为由new
绑定到被调用函数的this后返回一个对象,与Java等语言中类的构造方法完全是两码事。
可以看出字面量10所对应的基本类型为"number",,它并非"Number"对象的实例。而由 new关键字
返回的对象如new Number(10)
对应的typeof返回的结果表现为"[Number: 10]
",它是"Number"对象的实例。那么本例中Number(...)
与new Number(...)
到底有什么不同呢?我们先抛出这个问题,后续我们在对关键字new的讲解中继续讨论之。
顺便指出,以下是某教程的截图,其内容是不准确的:
另外需要特别指出的是:
typeof null
Out[]:
object
这是来自于JavaScript自身的bug之一,并不是null为object类型。再次强调,null
并非对象(object)类型也不具备对象的特点。为什么会有这个bug呢?其实null值表示一个空对象指针,正因为如此在JavaScript语言底层的二进制表示中,object类型前三位都是0,恰巧null类型的前三位也是0,typeof
在判断时就直接依此判成了object。
文内参考链接:typeof操作符的返回值
2. JavaScript 对象概述
2.1 JavaScript 中的对象是什么
JavaScript中的对象是拥有属性和方法的数据,对象为object类型及其子类型(对象子类型),看起来是以键值对形式展示:
var student = {firstName:"Barack", lastNme:"Obama"};
对于JavaScript对象,可以通过键来访问值:
student.firstName // 也可以写成student["firstName"]
Out[]:
“Barack”
对于以上两种方法,前一种“.
”访问要求属性名满足标识符命名,而后一种“["..."]
”的访问方式可以接收任意编码格式支持的字符串作为属性名。
对象中除了“属性”还可以含有函数(在JavaScript中准确说只能称为函数而不能称作方法)
let car = { color: "红", run : function() { return this.color + "色的小轿车奔跑在希望的道路上" } } car.run()
Out[]:
红色的小轿车奔跑在希望的道路上
2.2 JavaScript中的new XXX()
在做什么?
在JavaScript中new表达式执行的不是所谓构造函数
,也不存在所谓构造函数,而是 构造 + 对某个函数的 调用 ,也就是说new表达式只不过是一种函数调用的形式,其具体过程为:
- 创建一个新的JavaScript 对象 ;
- 执行该对象原型链连接;
- 将新对象绑定到函数调用的this;
- 函数不返回其它对象时,表达式返回这个新对象。
例如:
function node(data){ this.data = data; this.next = null } let nd = new node('data') let _ = node('data') console.log(nd); console.log(_); console.log(nd.data);
Out[]:
node { data: 'data', next: null } undefined data
在nodejs环境运行如图:
new node('data')
通过new操作符构造一个新的对象并将其绑定到new后函数node(‘data’)调用中的this上,并返回这个对象。
通过比较我们发现:
在没有使用
new
关键字时,由于函数没有返回值其输出的结果为undefined
,而使用new
关键字后,输出的是一个node对象,并可以看到其返回这个node对象的描述:node { data: 'data', next: null }
,这个是new关键字绑定并返回的对象,这种绑定方式也称作new绑定
。
现在我们回过头再看Number(...)
与new Number(...)
。已经说过,作为内置对象,Number()
就是一个函数。而这个函数返回的是传入其的数字,因此函数Number(10)
返回的为数字(number)10
。而在其前加一个new
关键字后,执行new绑定
将即将返回对象绑定到Number(…)函数在调用中的this
上。
2.3 如何克隆一个JavaScript对象
由于这部分内容较多,已单独成文。请参考博文链接:
【博文链接】:Javascript中的对象拷贝(对象复制/克隆)
2.4 模拟new
关键字的行为
// analog_new(Constr, ...args) 模拟 new Constr(...args) function analog_new (Constr,...args){ let obj={}; // 创建一个空的简单JavaScript对象(即**`{}`**); Object.setPrototypeOf(obj, Constr.prototype); // 新对象接收构造函数原型 const rt = Constr.apply(obj,args); // 使用指定的参数调用构造函数Constr()并将 this 绑定到新创建的对象。 if(rt instanceof Object) return rt; else return obj; }
3. 内置对象(也称作内置函数、原生函数)
3.1 内置对象概述
而如Number
这些大写字母开头的这些究竟是什么呢?——它们就是所谓是内置对象,也就是内置函数。
JavaScript中的内置对象以大写字母开头,本质上是一些内置函数
。需要指出JavaScript中也没有类的概念,即使ES6语法中的类也不是正真意义上的类而只是近似类的语法元素或语法糖。不过从功能上看,这些内置对象的功能类似于Java语言的类库提供的一些对象,但不是类。这些内置对象(内置函数)们可通过new
关键字产生函数的调用以构造一个对于子类型的新对象。
在这里,你看到的[String: 'Hello World']
与之前的[Number: 10]
都是当输出对象为函数时的描述。不仅仅是这些内置函数,对于一般的自定义函数于此类似:
内置对象如:
内置对象 | 说明 | 备注 |
Boolean |
布尔值对象 | 即布尔值的包装函数 |
Number |
数值对象 | 即原始数值的包装函数 |
String |
字符串对象 | 即字符串类型的包装函数 |
Symbol |
表示独一无二的值的对象 | ES6新增 |
BigInt |
表示大于 2 53 − 1 2^{53} - 1253−1 的整数对象 | ES7新增 |
Object |
对象对象 | 构造函数创建一个对象包装器 ,在JavaScript中,几乎所有的对象都是Object类型的实例 |
Function |
函数对象 | 函数也是功能完整的对象 |
Array |
数组对象 | 用于在单个的变量中存储多个值 |
Date |
日期对象 | 用于处理日期和时间 |
Regexp |
正则表达式对象 | 对象表示正则表达式,它是对字符串执行模式匹配的强大工具 |
Error |
错误对象 | 一般当代码运行时的发生错误时会自动创建新的Error 对象并将其抛出 |
释疑: object
与 Object
、typeof
关键字与 instanceof
关键字
★object
与 Object
上文已经说过:
- object 是表示非原始类型的基本类型,也就是除number,string,boolean,symbol,bigint, null或undefined之外的类型。
- 而Object是用于创建一个对象的包装器函数。在JavaScript中几乎所有的对象都是Object类型的实例,它们都会从Object.prototype继承属性和方法。据给定的参数不同,Object对象(函数)返回不同的结果:
- 1.如果给定值是 null 或 undefined,将会创建并返回一个空对象;
例如:
let obj_1 = new Object(); let obj_2 = new Object(undefined); let obj_3 = new Object(null);
- 2.如果传进去的是一个基本类型的值,则会构造其包装类型的对象;
例如:
let obj_4 = new Object(true); // 等价于 new Boolean(true) let obj_5 = new Object('hello'); // 等价于 new String('hello')
- 3.如果传进去的是
引用类型的值
,仍然会返回这个值,经他们复制的变量保有和源对象相同的引用地址
; - 4.当以非构造函数形式被调用时,Object 的行为等同于 new Object()。
★typeof 关键字
返回表示未经计算的操作数的类型的字符串
通过typeof操作符返回的结果是表示操作数的类型的字符串,可见在JavaScript中其返回的结果基本是确定的:
- 对于与简单基本类型对应的非引用对象:
类型 | 返回的字符串 | 备注 |
Undefined | undefined | |
Null | object | |
Boolean | boolean | |
Number | number | |
String | string | |
Symbol | symbol | ES6 新增 |
BigInt | bigint | ES7 新增 |
- 对于引用对象或其它复杂对象
类型 | 返回的字符串 | 备注 |
宿主对象 | 取决于具体实现 | 由 JS 环境提供的对象,如 alert() |
Function对象 | function | 按照 ECMA-262 规范实现 [[Call]] |
其他任何对象 | object |
- typeof 的使用例如:
typeof undefined // 返回值为字符串undefined let a; // ( 即a 是一个已经声明但未初始化的变量 ) typeof a // 返回值为字符串undefined typeof b // b 是一个未声明的变量时,返回值为字符串undefined typeof 3.14 // 返回值为字符串 number typeof(10) // 返回值为字符串 number typeof Number(1) // 返回值为字符串 number,上文已经说过,Number()对象(内置函数)返回数值 typeof NaN // 返回值为字符串 number typeof Infinity // 返回值为字符串 number typeof Symbol('foo') // 返回值为字符串 symbol, ES6(ECMA Script 2015新增类型) typeof Symbol.iterator // 返回值为字符串 symbol, ES6(ECMA Script 2015 新增类型) typeof 42n // 返回值为字符串 bigint , ES7(ECMA Script 2020 新增类型) typeof 'hello' // 返回值为字符串 string typeof true // 返回值为字符串 boolean typeof {a: 1} // (普通对象)返回值为字符串 object typeof [1, 2] // (数组)返回值为字符串 object typeof function(){} // (函数)返回值为字符串 function typeof class MyClass {} // (类,ES6以后版本的语法糖)返回值为字符串 function let func = new Function() typeof func //(函数)返回值为字符串 function
- 应用:通过
typeof
关键字如何检测某个变量obj是否是一个对象:
有一种典型的错误方法:
typeof obf === 'object'
- 绝大多数情况下这个语句是没问题的,不过细心的你可能想起在上文中我们提到过JavaScript中有一个bug,那就是
typeof null
也会返回"object"
,虽然曾有人提过该bug的修复方案,但至今未得以通过。因此,我们还需要进一步区分是null
值还是对象。其思路为:
let str = Object.protoytpe.toString; str.call(null) // 返回"[object Null]" str.call({}) // 返回"[object Object]"
- 为了能够一次性获取正确的类型,我们不妨自己动手对
typeof
进行封装,使封装后亦能够区分null和对象:
function getType(obj){ if(typeof obj==="object"){ let str = Object.prototype.toString; if(str.call(obj)==="[object Object]"){ return "object" }else{ return "null" } } else{ return typeof obj } } console.log(getType(1)); // 传入一个数值 console.log(getType({})); // 传入一个对象 console.log(getType(null)); // 传入一个 null
Out[]:
number object null
★instanceof 关键字
用于检测 构造函数的 prototype 属性
是否出现在某个实例对象的原型链
上
- instanceof 的使用例如:
function Func(param) { this.param = param; } const func = new Func('p1'); // 执行new绑定,构造调用Func获取新对象func console.log(func instanceof Func); console.log(func instanceof Object); console.log(Func instanceof Object);
Out[]:
true true true
★小节:运算符
typeof
和instanceof
的区别:
- typeof 用于检测数据类型,只有一个操作数,其返回一个表示数据类型的全小写字母的字符串;
- instanceof 用于检测两个对象的之间关联性,它有前后两个操作数,其返回的为一个布尔值;
- typeof的操作数可以是基本类型或者函数;
- instanceof 的 前一个操作数 不能是基本类型(基本类型非对象,故这样写无意义), 后一个操作数 必须是函数。
3.2 关于JavaScript数组对象的说明
在JavaScript中对象是拥有属性和方法的数据,并且对于一个普通对象,它看起来是以一对大括号包含的若干个键值对形式表达的。而数组却似乎有所不同,如:
let a = ['A0', 'A1', 0 , 1];
但实际上,数组同样是对象,并且也可以给数组添加属性,如:
let a = ['A0', 'A1', 0 , 1]; a.hello = 'hello' console.log(a); console.log(a['hello']); console.log(a.hello);
Out[]:
[ 'A0', 'A1', 0, 1, hello: 'hello' ] hello hello
可以看到对数组a
设置的属性hello
同样可以支持[]
和.
这两种属性访问形式。但同时需要指出,当使用[]
的访问形式时也有不同情况,即如果传入的是数字则表示数组的数值下标,仅仅传入相应字符串时才会被当做属性名处理。所谓"数值下标",指的是值在数组中存储的位序,也就是索引,它从0开始计算。
这里有一个迷惑性的行为,如果你对一个数组添加了一对"属性/属性值",将其打印到控制台上你将能看到似乎这对"属性/属性值"也在表示数组的[]内,于是你试图按照打印的位置将其按照索引取出,但会发现这是徒劳的!如:
let a = ['A0', 'A1', 0 , 1]; a.hello = 'hello' console.log(a); console.log(a[4]);
Out[]:
[ 'A0', 'A1', 0, 1, hello: 'hello' ] undefined
实际上这仅仅是JavaScript中一个不合理的输出方式。事实上,在JavaScript中给数组添加的普通属性不会影响到其按照为序的访问方式,而为其按照普通对象那样添加的属性与属性值只是在打印输出是显示在了表征数组的一对[]内的后面罢了。比如再次将上面代码修改:
let a = ['A0', 'A1', 0 , 1]; a.hello = 'hello' a.push(2) console.log(a); console.log(a[4]);
Out[]:
[ 'A0', 'A1', 0, 1, 2, hello: 'hello' ] 2
你会看到,明明后尾插了一个数值2,添加的属性却仍在最后显示。仅此而已。并且为数组添加的属性页不会影响到原数组的.length
等内建属性值。
也就是说,在JavaScript中的数组在普通对象的基础上建立了一套类似于其它语言中动态列表那样有利于结构化的值存储机制,这与数组作为对象在使用上具有相对的独立性。 因此从功能上看你完全可以把数组当成普通对象来使用,但是不推荐这样,因为数组和普通对象在其相应的优化。在无特殊功能需求的情况下,不建议用数组来存储普通对象那样的属性与属性值对。
这里还有一个让人意外的地方,就是由于 JavaScript数组允许用字符串数字来隐式转换为number类型的相应数字进行索引,因此,在将数组作为普通对象使用时,采用[]
添加属性/属性值时是无法做到使用一个字符串数字的,如:
let a = ['A0', 'A1', 0 , 1]; a['4'] = 2
这样将为数组 a 为序为 4 处添加一个元素,其值为 2,相当于:
a[4] = 2
细心的你可能会想起,之前我们说过,如果采用.
的方法来索引对象的属性,要求这个属性名必须满足标识符的命名规范。然而,存数字是不可以作为标识符的。因此对了:这两种方式是都无法让数组对象拥有一个纯数字属性的,这是和普通对象不一样的地方。
3.3 关于JavaScript函数对象的说明
1. JavaScript函数是 Function 对象
在JavaScript中函数也是一种特殊的对象,具体来说每个 JavaScript 函数实际上都是一个 Function 对象
。
要获取一个函数对象,既可以直接使用function关键字,也同样可以使用构造调用和非构造调用的方法。
使用function
关键字定义函数
格式为:
function fucName(...){ // function body [return] }
其中return
是可选的,表示函数的返回值。如:
function adder(a, b){ return a + n }
构造调用创建动态函数
new Function ([arg1[, arg2[, ...argN]],] functionBody)
参数:
- arg1, arg2, … argN
被函数使用的参数的名称必须是合法命名的。参数名称是一个有效的JavaScript标识符的字符串,或者一个用逗号分隔的有效字符串的列表;例如“×”,“theValue”,或“a,b”。 - functionBody
一个含有包括函数定义的 JavaScript 语句的字符串。
如:
let fuc = new Function("pram1", "pram2", "return pram1 + pram2") fuc(1,2) // 结果为3
非构造调用创建函数
以调用函数的方式调用 Function 的构造函数(而不是使用 new 关键字) ,即非构造调用,这跟以构造函数来调用的结果是一样的,返回的仍然是一个新的函数对象。
如:
let fuc = Function("pram1", "pram2", "return pram1 + pram2") fuc(1,2) // 结果为3
不过不得不提的是,虽然使用 使用 Function 构造器 以生成函数对象是可行的,单这并不是一种由效率的方式。Function 构造器生成的 Function 对象是在函数创建时解析的,这比使用函数声明或者函数表达式并在你的代码中调用更为低效,因为使用后者创建的函数是跟其他代码一起解析的。
2. JavaScript 函数可以像普通对象那样拥有属性
JavaScript 函数可以像普通对象那样拥有属性,被拥有的属性也可以是函数。借鉴于Java等语言的称为,作为JavaScript对象属性出现的函数,我们称之为“方法”。实际上在JavaScript中的函数常常用来模仿基于类的面向对象编程语言中的类,这在后面有单独的小节进行介绍和实现。
以下是JavaScript函数通过 .
语法 为其添加属性与属性值(以下是Node.js环境上的输出):
let adder = function (a,b){ return a + b } adder.a = 'a' console.log(adder); console.log(adder.a); console.log(adder(1,2));
Out[]:
[Function: adder] { a: 'a' } a 3
在chrome浏览器环境控制台的输出效果如下:
3. Function对象的原型属性(/方法)
一个 Function 对象自身在不为其添加其它属性时,它没有自己的属性和方法。但是,因为它本身也是一个函数,所以它也会通过原型链从自己的原型链 Function.prototype 上继承一些属性和方法。因此我们有必要了解一个函数从原型链上继承来的属性(/方法)。
属性/方法名 Function.prototype. |
说明 | 备注 |
name |
获取函数的名称 | |
constructor |
声明函数的原型构造方法 | 可参考 Object.constructor |
length |
获取函数的接收参数个数 | 也就是说对于函数而言,定义接收参数个数为其length |
apply() |
在一个对象的上下文中应用另一个对象的方法,参数能够以数组形式传入 | 调用一个具有给定this值的函数,以及以一个数组(或类数组对象)的形式提供的参数 |
bind() |
创建一个新函数,称为绑定函数 | 当调用这个绑定函数时,绑定函数会以创建它时传入 bind()方法的第一个参数作为 this 传入 bind()方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数 |
call() |
在一个对象的上下文中应用另一个对象的方法, 参数能够以列表形式传入。 |
call()方法的作用和 apply() 方法类似,区别就是call()方法接受的是参数列表,而apply()方法接受的是一个参数数组 |
toString() |
获取函数的实现源码的字符串 | 覆盖了 Object.prototype.toString 方法 |
例如:
function func(a, b) {} console.log(func.length);
Out[]:
2
function doSomething() { } console.log(doSomething.name;)
Out[]:
doSomething
// 定义一个函数 function Func1(firstName, secondName) { this.firstName= firstName; this.secondName= secondName; } // 定义另一个函数 function Func2(firstName, secondName) { Product.call(this, name, price); this.post= 'professor'; } console.log(new Func2('Jack', 'Lee').firstName);
Out[]:
Jack
可以看到,函数Func2
本来没有firstName
属性,但是通过.call()
方法进行调用Func1
后,获得了这个属性。
const numbers = [1, 2, 3, 4, 5]; const max = Math.max.apply(null, numbers); console.log(max);
Out[]:
5
function adder(a, b) { return a + b; }; console.log(adder.toString());
Out[]:
function adder(a, b) { return a + b; }
4. 对象的属性及其描述
4.1 属性
在JavaScript中对象的属性以键值对的方式进行存储,其键只支持字符串值的字面量(string),因此你不需要给作为属性的键加上引号。这对于新手可能产生一些迷惑,他们试图将布尔值、数值等等作为键似乎也能给对象添加属性:
let a = {}; a.true = "1"; a[2] = 2; console.log(a);
Out[]:
{ ‘2’: 2, true: ‘1’ }
但实际上在内部,这些所谓的数值、布尔值等,都是被当作字符串来使用的,如:
JavaScript的对象中的属性可以是字面值,也可以是其它对象(包含函数、数组等)。但往往其它对象并不会真的“属于”该对象,只不过是创建了一个对存储在其它位置的对象的引用。比如:
let car = { color: 'red', run: function(){ console.log('car is running.') } }
对于对象car的run属性,它是一个函数类型的对象,这里的run只不过是建立了到后面那个函数的引用,存储在对象car中的往往是这个引用run,而不包含被引用的函数。也就是说,他们在不同的内存空间。
3.2 ★属性的描述(重要)
在上文的代码中,我们要么直接创建一个对象并通过键值对给出对象的属性与值,要么通过.
或[]
的方法建立属性与值得键值对。但是这样并不能控制属性得具体行为,如:是否是可修改、是否可枚举等等,而这就是属性描述得意义所在。从ES5开始,提供了属性描述符用以控制某些属性得具体行为。
属性描述符有哪些
PropertyDescriptor(属性描述符)用于声明一个属性是否可以写入到是否可以删除可以枚举和指定内容的对象,它是可以具有以下键值的对象
字段名称 | 描述 |
getter |
该 get 语法将对象属性绑定到将在查找该属性时调用的函数。 |
setter |
set 当尝试设置该属性时,该 语法将对象属性绑定到要调用的函数。 |
value |
描述指定属性的值,可以是任何有效的 Javascript 值(函数、对象、字符串…) |
configurable |
声明该属性可以修改和删除 |
enumerable |
声明属性可以被枚举,可枚举属可以被for…in循环遍历。 |
writable |
声明指定的属性是否可以重写 |
获取属性及其描述信息——使用getOwnPropertyDescriptor()
在自己定义属性以及描述前,我们可以先获取一个属性默认的描述。这要用到Object.getOwnPropertyDescriptor()
,其格式为:
Object.getOwnPropertyDescriptor(对象名,"属性名");
如:
let a = {hello: 'world'} console.log(Object.getOwnPropertyDescriptor(a,'hello'))
Out[]:
{ value: 'world', writable: true, enumerable: true, configurable: true }
可以看到,一个属性的信息包含四个部分:“value”、“writable”、“enumerable”、“configurable”。这里的value也就是我们直接定义键值对时对应属性名作为键时的值。
而后面的“writable”、“enumerable”、“configurable”依次对应"属性值是否可修改"、“属性是否允许被defineProperty()配置”、“属性是否被允许出现在该对象的属性枚举中”。我们看到,这三个属性描述符对应的值都为true。
设置属性及其描述信息——使用defineProperty()
我们可以通过Object.defineProperty()
来显示设置属性及其描述信息,其格式为:
Object.defineProperty(对象名,"属性名"{ value: 值, writable: 布尔值, configurable: 布尔值, enumerable: 布尔值 });
如:
let a = {} Object.defineProperty(a,'1',{ value: 1, writable: true, configurable: true, enumerable: true });
这时,输出a将得到{ '1': 1 }
的一个对象。这个对象中的属性’1’,就是通过defineProperty
所添加的,它允许写入,及可以使用如a['1'] = 2
这样的方式去更改对应的属性值。它允许配置,也就意味着之后还可以再次使用defineProperty
进行配置。它允许枚举,这意味着会出现在对象的属性枚举中。
3.3 属性存取器(访问描述)
属性存取器包括了Getter
和Setter
,本质上这也属于属性描述的一部分,但他们不再叫“数据描述符”而叫“访问描述符”。使用访问描述符时,JavaScript将忽略defineProperty中的** value和 writable属性,而关心set和get**特性。
以例说明Getter
和Setter
的用法:
let car = { // 为对象car定义一个Getter get color(){ return this._color; }, // 为对象car定义一个Setter set color(color){ this._color = color; } } car.color = 'red' console.log(car.color);
Out[]:
red
再上例中,成对定义了Getter
和Setter
。当我们执行赋值操作car.color = 'red'
时,将右侧的值’red’作为参数传入相应color的Setter函数,该函数将’red’值绑定到对象’car’的’_color’属性上。而最后语句car.color
是获取car的color,它执行了对象’car’的相应color的一个Getter函数,该函数返回值为’car’对象的’_color’属性,也就是我们在Setter中设置的’red’。
4. 原型与JavaScript的继承
4.1 原型与原型链
1. 原型的概念
JavaScript 是一种所谓“基于原型的语言”,其每个对象都拥有一个所谓“原型对象”。而“对象原型”这一机制使得 JavaScript 对象 可以从其他 JavaScript 对象中继承属性。
所谓原型,其实就是JavaScript对象的一个特殊的属性
。
而 一个对象的原型也可能有其原型,以此层层继承形成的链式关系
,即所谓“原型链” 。
多数面向对象编程语言,如Java中,继承可以使得子类拥有父类的属性和方法,在类中定义的所有属性和方法都被复制到实例中。而在Javascript语言中,通过原型链继承的属性并不属于对象实例本身。事实上JavaScript 中并没有像其它语言那样复制JS中某个对象原型的属性,而是在对象实例和它的构造器之间建立一个链接。属性和方法定义在该对象的构造器函数上的prototype属性上。
这也就是说, JavaScript中并没有类似于类的复制机制,也不能够创建一个类的多个实例,只能创建原型链关联到同一个对象的多个对象
2. [[Prototype]]原型链的"尽头"
几乎JavaScript中的所有对象都来自 Object
对象,并且它们都从Object.prototype
继承“方法”和属性,尽管这些“方法”与属性可能被覆盖。这就意味着普通的[[Prototype]]链最终都将指向Object.prototype
。
【注】:其他构造函数的原型可以覆盖
constructor 属性
并提供自己的同名方法。例如,Number对象将覆盖Object的constructor 属性
并提供自己的toString()
方法。
3. Object.prototype对象
的常用 属性
和 "方法"
- 属性
属性 | 说明 | 备注 |
constructor |
返回创建实例对象的 Object 构造函数的引用 | 此属性的值是对函数本身的引用,而不是一个包含函数名称的字符串 |
__proto__ |
指向当对象被实例化的时候,用作原型的对象 | 已经弃用/不推荐 |
- 用例
let obj_1 = {}; console.log(obj_1.constructor === Object); let obj_2 = [] console.log(obj_2.constructor === Array); let obj_3 = new String('hello') console.log(obj_3.constructor === String);
Out[]:
true true true
- 方法
“方法” | 说明 | 备注 |
hasOwnProperty () |
返回一个布尔值 ,表示某个对象是否含有指定的属性 | 此属性非原型链继承的 |
isPrototypeOf () |
返回一个布尔值,表示指定的对象是否在本对象的原型链中 | |
propertyIsEnumerable () |
判断指定属性是否可枚举 | 此方法可以确定对象中指定的属性是否可以被 for…in 循环枚举,但是通过原型链继承的属性除外。如果对象没有指定的属性,则此方法返回 false。 |
toLocaleString () |
直接调用 toString()方法 | |
toString () |
返回对象的字符串表示 | 默认情况下,toString() 方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 “[object type]”,其中 type 是对象的类型。 |
valueOf () |
返回指定对象的原始值 | 默认情况下,valueOf方法由Object后面的每个对象继承。 每个内置的核心对象都会覆盖此方法以返回适当的值。如果对象没有原始值,则valueOf将返回对象本身。 |
- 用例
Out[]:
4. 原型链与属性设置
4.2 对象属性的存在性、枚举以及遍历
在上文中已经提到过属性描述符 enumerable 用于描述该属性是某可枚举。这里进一步讨论之。
4.2.1 属性的存在性
4.2.2 属性的枚举与遍历
4.3 ★JavaScript中 对象的继承
4.4 如何模拟“类”
在下一节我们将介绍ES6(ECMA Script 2015)中引入的class
语法糖。不过长期以来,我们一直是通过JavaScript中的prototype
属性来模拟类的。当然,前文已经说过,JavaScript中并不存在类,我们只能尽一切去模拟类的行为。即使在ES6以后所新增的class也只不过是模拟在语法的简写方式,和Java等基于类的面向对象编程语言有本质不同。这里,我们容代码来讲解与示范如何使用JavaScript来模拟。
以下是模拟一个单链表节点模拟类:
var Lnode = /** @class */ (function () { function Lnode(data) {/** 相当于模拟构造方法,实现参数的初始化,该函数与其所有者同名 */ if (data != undefined) { // 若初始化非空结点 this.data = data; this.next = null; this.empty = false; } else { // 若初始化空结点(即值构建结点对象但实际上不存在结点) this.data = null; this.next = null; this.empty = true; } } return Lnode; }());
可以看到,我们通过一个表示函数的变量来模拟类,并且以注释/** @calss *
标记之。基于该模拟节点“类”,我们可以构建一个简单的单链表“类”:
var LinkList = /** @class */ (function () { function LinkList(datas) { /** 构造方法:初始化 */ this.head = new Lnode(); if (datas == undefined) { this.head.next = null; // head.next 为头指针,指向首元结点 this.length = 0; // 初始化链表长度 } else { for (var i = 0; i < datas.length; i++) { this.append(datas[i]); } this.length = datas.length; // 指定一组数据初始化则初始后长度为这组数据元素的个数 } }; LinkList.prototype.clear = function () { /** * 清空链表,只要把头结点干废,整个链表直接就清空了 */ this.head.next = null; this.length = 0; }; LinkList.prototype.append = function (elm) { /** * 用数据构成新结点插入单链表尾部 * @param elm {any} 需要插入链表尾部的新结点 */ if (this.head.next == null) { // 没有头结点 this.head.next = new Lnode(elm); this.length = this.length + 1; } else { var pointer = this.head.next; // 指向头节点 while (pointer.next != null) { pointer = pointer.next; }; pointer.next = new Lnode(elm); } }; }());
5. 类
这里要注意,在ES6语法中虽然有class关键字,但只不过是类似于其它语言中的类语法糖。JavaScript不存在类,其伪类(类似类,模仿类)无法描述对象的行为。使用new
关键字得到的也仅仅是相互关联的一个新对象,并不会得到一个对象复制到另一个对象形成的类的“实例”。
通过与原型创建关联得到的新对象的继承模式使得一个对象可以通过“委托”以访问另一个对象的属性。因此我们需要将类和继承的设计模式转变为委托行为的设计模式。
5.1 ES6中类的写法
ES6的class语法糖是从基于类的面向对象语言中借鉴来的,其底层仍然是基于原型链的。类语法的大概形式为:
class CameName [extends ...] { // class body constructor(pram1,...){ /** * 类构造器,底层相当于初始话一个JavaScript函数 * 可以在这初始化一些类参数 */ this.pram1 = pram1; ... } }
其中"…“表示省略的具体内容,”[]"表示可选部分。
5.2 使用class语法糖实现节点和单链表
上文中我们的节点模拟类和单链表模拟类写成ES6中class语法糖表达的类是什么样的呢:
(ES6的class语法糖写法:)
节点
class Lnode { constructor(data) { if (data != undefined) { // 非空结点 this.data = data; this.next = null; this.empty = false; } else { // 空结点,即值构建结点对象但实际上不存在结点 this.data = null; this.next = null; this.empty = true; } } } return Lnode;
单链表
class LinkList { constructor(datas) { /**初始化 */ this.head = new Lnode(); if (datas == undefined) { this.head.next = null; // head.next 为头指针,指向首元结点 this.length = 0; // 初始化链表长度 } else { for (var i = 0; i < datas.length; i++) { this.append(datas[i]); } this.length = datas.length; // 指定一组数据初始化则初始后长度为这组数据元素的个数 } } clear() { /** * 清空链表,只要把头结点干废,整个链表直接就清空了 */ this.head.next = null; this.length = 0; } append(elm) { /** * 用数据构成新结点插入单链表尾部 * @param elm {any} 需要插入链表尾部的新结点 */ if (this.head.next == null) { // 没有头结点 this.head.next = new Lnode(elm); this.length = this.length + 1; } else { var pointer = this.head.next; // 指向头节点 while (pointer.next != null) { pointer = pointer.next; }; pointer.next = new Lnode(elm); } } };
【提示】:一个拥有更多方法的ES6一个ES6单链表类的实现参见附录2
5.3 ES6"类"的继承
JavaScript 只有对象。每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。类的继承仍旧基于此,只是借用了基于类的面向对象编程语言中的“extends”关键字的用法,这也是一个语法糖,使用extends关键字时,不会像基于类的面向对象编程语言中那样,对父类属性和方法进行复制。在以前,我们要实现对象的继承,看起来是这样写的:
而使用extends,我们可以这样写:
实际上虽然书写形式发生了改变,但是两者的本质却还是一样的。
5.4 私有(private)与公共(public)
附录:
附.1 内置对象参考手册
附.1.1 Object对象
即Object() 构造函数
JavaScript中对象j几乎都是Object类型的实例,这些实例们会从Object.prototype
继承属性和方法。Object
构造函数为给定值创建一个对象包装器,其构造调用格式为:
构造调用:
new Object([value])
- 如果给定值是 null 或 undefined,将会创建并返回一个空对象
- 如果传进去的是一个基本类型的值,则会构造其包装类型的对象
- 如果传进去的是引用类型的值,仍然会返回这个值,经他们复制的变量保有和源对象相同的引用地址
【注】Object.create()方法
也可以用来创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。这种方法于new绑定
方法都同样得到一个新的对象。ES5 开始可以用 Object.create(null) 来创建一个没有原型的对象。
属性
属性Object. |
含义/功能 | 说明 |
length |
值为 1 | |
prototype |
可以为所有 Object 类型的对象添加属性 |
静态方法
静态方法Object. |
含义/功能 | 说明 |
assign() |
拷贝一个或多个对象来创建一个新的对象 | JavaScript对象拷贝详见博客 Javascript中的对象拷贝 |
create() |
使用指定的原型对象和属性创建一个新对象 | |
defineProperty() |
给对象添加一个属性并指定该属性的配置 | |
defineProperties() |
给对象添加多个属性并分别指定它们的配置 | |
entries() |
返回给定对象自身可枚举属性的 [key, value] 数组 | |
freeze() |
冻结对象:其他代码不能删除或更改任何属性 | |
getOwnPropertyDescriptor() |
返回对象指定的属性配置 | |
getOwnPropertyNames() |
返回一个数组,它包含了指定对象所有的可枚举或不可枚举的属性名 | |
getOwnPropertySymbols() |
返回一个数组,它包含了指定对象自身所有的符号属性 | |
getPrototypeOf() |
返回指定对象的原型对象 | |
is() |
比较两个值是否相同 | 所有 NaN 值都相等,这与和=不同 |
isExtensible() |
判断对象是否可扩展 | |
isFrozen() |
判断对象是否已经冻结 | |
isSealed() |
判断对象是否已经密封 | |
keys() |
返回一个包含所有给定对象自身可枚举属性名称的数组 | |
preventExtensions() |
防止对象的任何扩展 | |
seal() |
防止其他代码删除对象的属性 | |
setPrototypeOf() |
设置对象的原型 | 即内部 [[Prototype]] 属性 |
values() |
返回给定对象自身可枚举值的数组 |
附.1.2 Boolean对象
即Boolean() 构造函数
Boolean对象是一个布尔值的对象包装器,对于非构造调用它将第一个参数传入的值转换为布尔值。对于构造调用,它将传入的值返回一个布尔对象:
- 如果省略或值0,-0,null,false,NaN,undefined,或空字符串(""),该对象具有的初始值false;
- 所有其他值,包括任何对象,空数组([])或字符串"false",都会创建一个初始值为true的布尔对象。
以下是用例:
实例方法
实例方法Boolean.prototype. |
含义/功能 | 说明 |
toString() |
根据对象的值返回字符串"true"或"false" | 为重写Object.prototype.toString()方法 |
valueOf() |
返回Boolean对象的原始值 | 为重写Object.prototype.valueOf()方法 |
附.1.3 Number对象
即Number() 构造函数
JavaScript 中的数字均为双精度浮点类型(double-precision 64-bit binary format IEEE 754),是一个介于± 2 − 1023 ±2^{−1023}±2−1023和± 2 + 1024 ±2^{+1024}±2+1024间的数字,或约为± 1 0 − 308 ±10^{−308}±10−308到± 1 0 + 308 ±10^{+308}±10+308,数字精度为53 5353位。整数数值仅在± ( 2 53 − 1 ) ±(2^{53} - 1)±(253−1)的范围内可以表示准确。除了能够表示浮点数,数字类型也还能表示三种符号值: +Infinity(正无穷)、-Infinity(负无穷)和 NaN (not-a-number,非数字)
属性
属性Number. |
含义/功能 | 说明 |
MAX_VALUE |
可表示的最大值 | |
MIN_VALUE |
可表示的最小值 | |
NaN |
特指”非数字“ | Not a Number的缩写 |
NEGATIVE_INFINITY |
特指“负无穷”;在溢出时返回 | |
POSITIVE_INFINITY |
特指“正无穷”;在溢出时返回 | |
EPSILON |
表示1和比最接近1且大于1的最小Number 之间的差别 |
|
MIN_SAFE_INTEGER |
JavaScript最小安全整数 | |
MAX_SAFE_INTEGER |
JavaScript最大安全整数 |
方法
方法Number. |
含义/功能 | 说明 |
parseFloat() |
把字符串参数解析成浮点数 | 和全局方法 parseFloat() 作用一致 |
parseInt() |
把字符串解析成特定基数对应的整型数字 | 和全局方法 parseInt() 作用一致 |
isFinite() |
判断传递的值是否为有限数字 | |
isInteger() |
判断传递的值是否为整数 | |
isNaN() |
判断传递的值是否为 NaN |
|
isSafeInteger() |
判断传递的值是否为安全整数 |
原型方法
原型方法Number.prototype. |
含义/功能 | 说明 |
toExponential() |
返回一个数字的指数形式的字符 | 形如:1.23e+2 |
toFixed() |
返回指定小数位数的表示形 | let a=123,b=a.toFixed(2) b的值为"123.00" |
toPrecision() |
返回一个指定精度的数字 | 如假设a=123,3会由于精度限制消失let a=123,b=a.toPrecision(2) b的值为"1.2e+2" |
附.1.4 BigInt对象
即BigInt() 构造函数
BigInt 是一种ES7(ECMA Script 2020)的新增内置对象,它提供了一种方法来表示大于 253 - 1 的整数。相比于Number对象,BigInt 可以表示任意大的整数。
【注】: Number 与 BigInt 之间进行转换会损失精度,因而建议仅在值可能大于253 时使用 BigInt 类型,并且不在两种类型之间进行相互转换
- 略
附.1.5 String对象
即String() 构造函数
用于字符串或一个字符序列的构造函数。
基本字符串string和字符串对象String的区别
上文已经举例说过,字符串字面量string (通过单引号或双引号定义,如"hello") 和 直接调用 String() 函数 (没有通过 new 生成字符串对象实例) 的字符串都是基本字符串。JavaScript会自动将基本字符串转换为字符串对象,只有将基本字符串转化为字符串对象之后才可以使用字符串对象的方法。当基本字符串需要调用一个字符串对象才有的方法或者查询值的时候(基本字符串是没有这些方法的),JavaScript 会自动将基本字符串转化为字符串对象并且调用相应的方法或者执行查询。
原型属性
原型属性String.prototype. |
含义/功能 | 说明 |
constructor |
用于创造对象的原型对象的特定的函数 | |
length |
返回了字符串的长度 |
原型方法
原型方法String.prototype. |
含义/功能 | 说明 |
charAt() |
返回特定位置的字符 | |
charCodeAt() |
返回表示给定索引的字符的Unicode的值 | |
codePointAt() |
返回使用UTF-16编码的给定位置的值的非负整数 | |
concat() |
连接两个字符串文本 | 返回一个新的字符串 |
includes() |
判断一个字符串里是否包含其他字符串 | |
endsWith() |
判断一个字符串的是否以给定字符串结尾 | 结果返回布尔值 |
indexOf() |
从字符串对象中返回首个被发现的给定值的索引值 | 如果没有找到则返回-1 |
lastIndexOf() |
从字符串对象中返回最后一个被发现的给定值的索引值 | 如果没有找到则返回-1 |
localeCompare() |
返回一个数字表示是否引用字符串在排序中位于比较字符串的前面,后面,或者二者相同 | |
match() |
使用正则表达式与字符串相比较 | |
normalize() |
返回调用字符串值的Unicode标准化形式 | |
padEnd() |
在当前字符串尾部填充指定的字符串, 直到达到指定的长度 | 返回一个新的字符串 |
padStart() |
在当前字符串头部填充指定的字符串, 直到达到指定的长度 | 返回一个新的字符串 |
repeat() |
返回指定重复次数的由元素组成的字符串对象 | |
replace() |
被用来在正则表达式和字符串直接比较,然后用新的子串来替换被匹配的子串 | |
search() |
对正则表达式和指定字符串进行匹配搜索,返回第一个出现的匹配项的下标 | |
slice() |
摘取一个字符串区域,返回一个新的字符串 | |
split() |
通过分离字符串成字串,将字符串对象分割成字符串数组 | |
startsWith() |
判断字符串的起始位置是否匹配其他字符串中的字符 | |
substr() |
通过指定字符数返回在指定位置开始的字符串中的字符 | |
substring() |
返回在字符串中指定两个下标之间的字符 | |
toLocaleLowerCase() |
根据当前区域设置,将符串中的字符转换成小写 | 对于大多数语言来说,toLowerCase的返回值是一致的 |
toLocaleUpperCase() |
根据当前区域设置,将字符串中的字符转换成大写 | 对于大多数语言来说,toUpperCase的返回值是一致的 |
toLowerCase() |
将字符串转换成小写并返回 | |
toString() |
返回用字符串表示的特定对象 | 重写 Object.prototype.toString 方法 |
toUpperCase() |
将字符串转换成大写并返回 | |
trim() |
从字符串的开始和结尾去除空格 | 参照部分 ES5 标准 |
trimStart() |
从字符串的开头(左侧)删除空格 | |
trimEnd() |
从字符串的结尾(右侧)去除空格 | |
valueOf() |
返回特定对象的原始值 |
附.1.6 Date对象
JavaScript没有日期数据类型(没有date),但可以在程序中使用 Date 对象和其方法来处理日期和时间。
非构造调用
不使用 new 关键字来调用Date对象将返回当前时间和日期的字符串。
构造调用
通过构造调用创建Date对象:
var dateObj = new Date([parameters]);
参数
- 无参数 : 创建今天的日期和时间,例如: today = new Date();.
- 一个符合以下格式的表示日期的字符串: “月 日, 年 时:分:秒.” 例如: var Xmas95 = new Date(“December 25, 1995 13:30:00”)。如果你省略时、分、秒,那么他们的值将被设置为0。
- 一个年,月,日的整型值的集合,例如: var Xmas95 = new Date(1995, 11, 25)。
- 一个年,月,日,时,分,秒的集合,例如: var Xmas95 = new Date(1995, 11, 25, 9, 30, 0)。
附.1.7 Array对象
用于构造数组。
构造调用
构造调用返回的是一个新的数组对象。它有以下调用格式:
- 格式1
new Array(element0, element1[, ...[, elementN]])
- 格式2
new Array(arrayLength)
注:构造调用返回的数组,于直接使用[element0, element1, ..., elementN]
语法定义的数组是一样的。
属性
属性Array. |
含义/功能 | 说明 |
length | 其值为1 | 该属性为静态属性,不是数组实例的 length 属性 |
prototype | 通过数组的原型对象可以为所有数组对象添加属性 |
方法
方法Array. |
含义/功能 | 说明 |
from() | 从类数组对象或者可迭代对象中创建一个新的数组实例 | |
isArray() | 用来判断某个变量是否是一个数组对象 | |
of() | 根据一组参数来创建新的数组实例 | 支持任意的参数数量和类型 |
附.1.8 Function对象
每个 JavaScript 函数实际上都是一个 Function 对象。
构造调用创建动态函数
new Function ([arg1[, arg2[, ...argN]],] functionBody)
参数:
- arg1, arg2, … argN
被函数使用的参数的名称必须是合法命名的。参数名称是一个有效的JavaScript标识符的字符串,或者一个用逗号分隔的有效字符串的列表;例如“×”,“theValue”,或“a,b”。 - functionBody
一个含有包括函数定义的 JavaScript 语句的字符串。
如:
let fuc = new Function("pram1", "pram2", "return pram1 + pram2") fuc(1,2) // 结果为3
非构造调用创建函数
以调用函数的方式调用 Function 的构造函数(而不是使用 new 关键字) ,即非构造调用,这跟以构造函数来调用的结果是一样的,返回的仍然是一个新的函数对象。
如:
let fuc = Function("pram1", "pram2", "return pram1 + pram2") fuc(1,2) // 结果为3
附.1.9 Errors对象
附.1.10 RegExp对象
附.2 用JavaScript模拟类:模拟节点类与单链表类源码
模拟节点类
var Lnode = /** @class */ (function () { function Lnode(data) {/** 相当于模拟构造方法,实现参数的初始化,该函数与其所有者同名 */ if (data != undefined) { // 若初始化非空结点 this.data = data; this.next = null; this.empty = false; } else { // 若初始化空结点(即值构建结点对象但实际上不存在结点) this.data = null; this.next = null; this.empty = true; } } return Lnode; }());
模拟单链表类
var LinkList = /** @class */ (function () { function LinkList(datas) { /** 构造方法:初始化 */ this.head = new Lnode(); if (datas == undefined) { this.head.next = null; // head.next 为头指针,指向首元结点 this.length = 0; // 初始化链表长度 } else { for (var i = 0; i < datas.length; i++) { this.append(datas[i]); } this.length = datas.length; // 指定一组数据初始化则初始后长度为这组数据元素的个数 } }; LinkList.prototype.is_empty = function () { /** * 判断链表是否为空 * 只需要判断头结点是否为空,若头结点为空则为空链表,否则不是。 * @return {Boolean} true:链表为空; false:链表非空。 */ if (this.head.next == null) { return true; } else { return false; } }; LinkList.prototype.top = function () { /** * 获取链表头结点的数据域内容 */ return this.head.next.data; }; LinkList.prototype.top_node = function () { /** * 获取链表头结点 */ var node = this.head.next; node.next = null; return node; }; LinkList.prototype.clear = function () { /** * 清空链表,只要把头结点干废,整个链表直接就清空了 */ this.head.next = null; this.length = 0; }; LinkList.prototype.append = function (elm) { /** * 用数据构成新结点插入单链表尾部 * @param elm {any} 需要插入链表尾部的新结点 */ if (this.head.next == null) { // 没有头结点 this.head.next = new Lnode(elm); this.length = this.length + 1; } else { var pointer = this.head.next; // 指向头节点 while (pointer.next != null) { pointer = pointer.next; }; pointer.next = new Lnode(elm); } }; LinkList.prototype.append_node = function (node) { /** * 将新结点挂载到链表尾部 * @param node {Lnode} 需要插入链表尾部的新结点 */ if (this.head.next == null) { // 没有头结点 this.head.next = node; this.length = this.length + 1; } else { var pointer = this.head.next; // 指向头节点 while (pointer.next != null) { pointer = pointer.next; }; pointer.next = node; }; }; LinkList.prototype.append_left = function (elm) { /** * 用数据构成新结点插入单链表头部 * @param elm {any} 需要插入链表尾部的新结点 */ if (this.head == null) { this.head.next = new Lnode(elm); // 若为空链表,直接将链表头设置为该结点 this.length = this.length + 1; // 增加结点长度 } else { // 先将新结点的`next`指向原第一个结点 var node = new Lnode(elm); node.next = this.head.next; // 再将`head`指向新的结点 this.head.next = node; this.length = this.length + 1; // 增加结点长度 }; }; LinkList.prototype.append_node_left = function (node) { /** * 将一个新结点插入到链表首部 * @param node {Lnode} 要从左侧也就是链头插入的新结点 */ if (this.head.next == null) { this.head.next = node; // 若为空链表,直接将链表头设置为该结点 this.length = this.length + 1; // 增加结点长度 } else { // 先将新结点的`next`指向原第一个结点 node.next = this.head.next; // 再将`head`指向新的结点 this.head.next = node; this.length = this.length + 1; // 增加结点长度 }; }; LinkList.prototype.get_node = function (index) { /** * 获取索引号为`index`的结点。 * 索引号是从数字`0`开始计算的 * @param index {number} 索引号 * @return node {Lnode} 索引得到地节点 */ if (index < 0) { throw "ValueError: Index must be a positive integer!"; } // 负数索引报错 else if (index + 1 > this.length) { throw "ValueError: Index exceeds the maximum value!"; } // 索引过界报错 else { var pointer = this.head.next; // 从头结点开始 for (var i = 0; i < index; i++) { pointer = pointer.next; // 逐个指向下一个结点 }; pointer.next = null; // 斩断后继 return pointer; }; }; LinkList.prototype.get = function (index) { /** * 索引结点地数据 * @param index {number} 索引号 * @return data {any} 链表中与索引号对应地节点地数据域内容 */ return this.get_node(index).data; }; LinkList.prototype.index = function (x, strict) { if (strict === void 0) { strict = true; } /** * 返回链表中第一个数据域 x 的元素的零基索引 * @param x {any} 用于寻找索引的数据 * @param strict? {boolean} 是否开启严格模式,如果不开启严格模式在统计时相同的不同类型 */ var pointer = this.head; var index = 0; // 当前考察结点的索引号,从0开始计算(所谓零基) if (strict == true) { while (pointer.next) { if (pointer.next.data == x && typeof (pointer.next.data) == typeof (x)) { return index; } else { console.log(pointer.next.data, x); console.log(pointer.next.data == x, typeof (pointer.next.data) == typeof (x)); index++; pointer = pointer.next; } } } else { while (pointer.data != x) { index++; pointer = pointer.next; } } return index; }; LinkList.prototype.count = function (x, strict) { if (strict === void 0) { strict = true; } /** * 返回链表中结点的值域存储内容与x内容相同的结点的个数 * @param x {any} 用于比较的数据 * @param strict? {boolean} 是否开启严格模式,如果不开启严格模式在统计时相同的不同类型 * 可能同时被统计,如数字 1 和字符串 "1" * @returns counter {number} 被统计对象在链表数据域中出现的次数 */ var counter = 0; var pointer = this.head.next; while (pointer) { if (pointer.data == x) { if (!strict) { // 不严格判断类型模式 counter++; } else { // 严格判断类型模式 if (typeof (pointer.data) == typeof (x)) { counter++; } } }; pointer = pointer.next; }; return counter; }; LinkList.prototype.insert = function (index, node) { /** * 在链表的第`index`个节的位置挂载新的结点`node`,其中从结点为`index = 0`开始编号 * 也就是说,新的结点索引号将为`index`,而这个结点将挂载到索引号为`index-1`的结点后面 * * @param index {number} 新结点插入的索引号 * @param node {Lnode} 要插入的新结点 */ if (node != null && node.next == null) { node.next = null; }; // 只允许插入单个结点,若有后继直接切断 if (index == 0) { node.next = this.head; // 先收小弟 this.head = node; // 再上位 } else { var pointer = this.head; // 从头结点开始 if (index == 0) { this.append_node_left(node); } else { for (var i = 0; i < index - 1; i++) { // 调整指针所指,即调整引用对象位置 pointer = pointer.next; // 逐个指向下一个结点,直至`pointer`指向被索引结点的前一个结点 } node.next = pointer.next; // 先收小弟 pointer.next = node; // 再上位 this.length = this.length + 1; // 更新结点长度 }; }; }; LinkList.prototype.to_array_node = function () { /** * 获取链表结点依次构成的数组 * @returns elem {Lnode[]} 以此装载了被遍历到的所有结点(这里其中每个结点都是孤立、干掉next的) */ var elm = []; // 准备好用于容纳所有的结点 var pointer = this.head.next; // 挂载操作一开始时指针指向头部结点、 if (pointer == null) { // 空链表,不仅要返回一个空数组,还要抛出提示 console.warn("Warning: It seems that you hava traversed an empty Linked List!"); return elm; } else { // 非空链表 while (pointer.next != null) { // 存在下一个结点时 if (pointer == null) { // 停止(拦截)条件:(直到)结点为`null` break; } else { // 获取当前结点剔除链接关系的`孤立结点`挂载结点数组 elm.push(new Lnode(pointer.data)); }; pointer = pointer.next; // 指向后继 }; elm.push(new Lnode(pointer.data)); // 最后一个元素 return elm; }; }; LinkList.prototype.to_array = function () { /** * 获取链表结点依次构成的数组 * @returns elem {Lnode[]} 以此装载了被遍历到的所有结点(这里其中每个结点都是孤立、干掉next的) */ var elm = []; // 准备好用于容纳所有的结点 var pointer = this.head.next; // 挂载操作一开始时指针指向头部结点、 if (pointer == null) { // 空链表,不仅要返回一个空数组,还要抛出提示 // console.warn("Warning: It seems that you hava traversed an empty Linked List!"); return elm; } else { // 非空链表 while (pointer.next != null) { // 存在下一个结点时 if (pointer == null) { // 停止(拦截)条件:(直到)结点为`null` break; } else { // 获取当前结点剔除链接关系的`孤立结点`挂载结点数组 elm.push(pointer.data); }; pointer = pointer.next; // 指向后继 }; elm.push(pointer.data); // 最后一个元素 return elm; }; }; LinkList.prototype.replace = function (index, data) { /** * 替换指定索引处结点的数据 * 该方法不会改变结点的连接关系 */ if (index < 0) { throw "ValueError: Index must be a positive integer!"; } else if (index + 1 > this.length) { throw "ValueError: Index exceeds the maximum value!"; } var pointer = this.head; var ct = 0; while (ct < index) { // 指针逐渐指向被修改数据的前一结点 pointer = pointer.next; ct++; }; pointer.next.data = data; // 修改结点的数据域 }; LinkList.prototype.replace_node = function (index, node) { /** * 替换指定索引处的结点 */ if (index < 0) { throw "ValueError: Index must be a positive integer!"; } else if (index + 1 > this.length) { throw "ValueError: Index exceeds the maximum value!"; } var pointer = this.head; var ct = 0; while (ct < index) { pointer = pointer.next; ct++; }; node.next = pointer.next.next; pointer.next = node; }; LinkList.prototype.drop = function (index1, index2) { if (this.head == null) { throw "EelEmptyListError: Unable to pop up node from an empty linked list."; } else { if (index2 == undefined) { /** * 给定一个索引号`index1`时 * 删除索引号为`index1`的某个结点及其之后的所有结点 * @param index {number} 结点索引号,该索引号指示结点及其之后全部结点都将从链表中删除 */ if (index1 < 0) { throw "ValueError: Index must be a positive integer!"; } else if (index1 + 1 > this.length) { throw "ValueError: Index exceeds the maximum value!"; } if (index1 == 0) { this.clear(); } else { var pointer = this.head.next; for (var i = 0; i < index1 - 1; i++) { pointer = pointer.next; // 指向下一个,直至指向索引结点的前一个 }; pointer.next = null; // 通过清空被索引结点的前一个结点的后继指针,将被索引系欸但及其后从链表删除 }; } else { /** * 给定两个索引号`index1`和`index2`时 * 删除索引号从`a`(包含)到`b`(包含)的一段结点 * 允许`a`和`b`任意一个索引值更大,即不区分区间起始大小。 * @param index {number[]} 包含两个索引值范围的数组。该范围内的所有结点都将从链表中删除。 */ // if(index1==index2){this.del(index1)} if (index1 == index2) { this.del(index1); } else if (index1 + 1 > this.length || index2 + 1 > this.length) { throw "ValueError: Index exceeds the maximum value!"; } else if (index1 < 0 || index2 < 0) { throw "ValueError: Index must be a positive integer!"; } else { var a = index1; var b = index2; if (a > b) { // 不论先前哪一个索引值更大,始终转换为a更小、b更大 a = index2; b = index1; } var pointer = this.head; // 引用头节点 if (a != 0) { for (var i = 0; i < a; i++) { // 处理结点范围为:[0:a-1] pointer = pointer.next; // 以`pointer`为指针,逐步指向下一个结点,直至指向索引a结点的前一个结点 }; for (var i = 0; i < b - a + 1; i++) { // 处理结点范围为:[a:b] pointer.next = pointer.next.next; // 以`pointer.next`为指针,逐步指向下一个结点,直到跳过中间`b-a+1`个结点 }; // 到此为止,后面[b+1,]的结点也是自动挂着的,无需再处理 } else { for (var i = 0; i < b + 1; i++) { // 处理结点范围为:[a:b] pointer.next = pointer.next.next; // 以`pointer.next`为指针,逐步指向下一个结点,直到跳过中间`b-a+1`个结点 }; // 到此为止,后面[b+1,]的结点也是自动挂着的,无需再处理 } } } } }; LinkList.prototype.del = function (index) { /** * 删除索引号为`index`的某个结点,保留其后继结点 * @param index {number} 将被删除的结点的索引号 */ if (index < 0) { throw "ValueError: Index must be a positive integer!"; } else if (this.head.next == null) { throw "EelEmptyListError: Unable to pop up node from an empty linked list."; } else if (index + 1 > this.length) { throw "ValueError: Index exceeds the maximum value!"; } else { if (index == 0) { this.pop_left(); } else { var pointer = this.head.next; // 创建指针(对象引用) for (var i = 0; i < index - 1; i++) { pointer = pointer.next; // 指向下一个,直至指向被索引结点的前一个结点 }; pointer.next = pointer.next.next; // 跳过被索引结点以实现删除之 }; }; }; LinkList.prototype.pop = function () { /** * 弹出最右边的一个结点并返回 * @returns node {Lnode} 原链表最右侧的一个结点 */ if (this.head.next == null) { throw "PopEmptyListError: Unable to pop up node from an empty linked list."; } else if (this.length == 1) { var last = this.head.next; this.head.next = null; return last; } else { var pointer = this.head; // 创建指针(对象引用) while (pointer.next.next != null) { pointer = pointer.next; // 指向下一个,直至指向倒数第二个结点 }; var last = pointer.next; pointer.next = null; this.length = this.length - 1; if (this.length == 0) { this.head = null; }; return last; }; }; LinkList.prototype.pop_left = function () { /** * 弹出最左边的一个结点并返回 * @returns node {Lnode} 原链表最左侧的一个结点 */ if (this.head.next == null) { throw "PopEmptyError: Unable to pop up node from an empty linked list."; } else if (this.length == 1) { var last = this.head.next; this.head.next = null; return last; } else { var pointer = this.head.next; this.head.next = this.head.next.next; // 头结点右移一个 pointer.next = null; // 斩断后继联系 this.length = this.length - 1; // 更新链表长度 if (this.length == 0) { this.head = null; }; return pointer; } }; LinkList.prototype.merge = function (list) { /** * 实现两个链表的合并 * 传入另外一个链表,将这个新传入的链表挂载到本链表的后边 * @param list{LinkList} 将要合并到本链表的一个新链表 */ var pointer = this.head; // 指向本链头链表指针,实际上是对链表结点对象的引用 while (pointer.next != null) { // 先将指针移动到本链表的尾部 pointer = pointer.next; }; pointer.next = list.head; // 将本链表的链尾结点指向新链表的链头结点 }; LinkList.prototype.reverse = function () { /** * 将原链表就地反转,不反返回何结果 * 该方法将会改变原链表 */ var pointer = this.head; // 引用头指针 var h = new Lnode(); // 新头指针 while (pointer.next) { // 若原链表还有结点 var node = this.pop_left(); // 原链表弹出第一个结点 if (h.empty) { // 空 h.next = node; h.empty = false; } else { node.next = h.next; h.next = node; }; }; this.head = h; }; LinkList.prototype.get_reverse = function () { /** * 反转链表 * 原链表不变,返回反转后的新链表 */ var pointer = this.head; // 引用头指针 var list = new LinkList(); // 一个新链表 while (pointer.next) { // 逐个元素出来 var node = this.pop_left(); list.append_left(node.data); }; return list; }; LinkList.prototype.reverse_by_k = function (k) { /** * 将链表中的所有结点以k个为一组进行翻转 * @param k{number}分组的结点元素个数 */ var pointer = this.head; // 引用头指针 var h = new Lnode(); // 新头指针 var chain_p = new Lnode(); // 用来引用每k个结点 var ct = 0; // 计数器 while (pointer.next) { // 若原链表还有结点 if (ct < k) { // 还不够k个结点,则增长子节点链 var node = this.pop_left(); // 原链表弹出第一个结点 if (chain_p.empty) { chain_p.next = node; chain_p.empty = false; } else { node.next = chain_p.next; chain_p.next = node; }; ct++; if (this.length == 0) { // 针对最后一段的挂载 var pt = h; while (pt.next) { pt = pt.next; }; pt.next = chain_p.next; }; } else { // 已经够k个结点,则将子结点挂载到新头,清空子结点链与计数器 if (h.empty) { h.next = chain_p.next; h.empty = false; } else { var pt = h; while (pt.next) { // 逐渐指向新子链表最后一个结点 pt = pt.next; } pt.next = chain_p.next; }; chain_p = new Lnode(); ct = 0; }; }; this.head = h; }; return LinkList; }());
附.3 用ES6 class语法糖的节点类与单链表类源码
ES6 节点类
class Lnode { constructor(data) { if (data != undefined) { // 非空结点 this.data = data; this.next = null; this.empty = false; } else { // 空结点,即值构建结点对象但实际上不存在结点 this.data = null; this.next = null; this.empty = true; } } } return Lnode;
SE6 单链表“类”
class LinkList { constructor(datas) { /**初始化 */ this.head = new Lnode(); if (datas == undefined) { this.head.next = null; // head.next 为头指针,指向首元结点 this.length = 0; // 初始化链表长度 } else { for (var i = 0; i < datas.length; i++) { this.append(datas[i]); } this.length = datas.length; // 指定一组数据初始化则初始后长度为这组数据元素的个数 } } is_empty() { /** * 判断链表是否为空 * 只需要判断头结点是否为空,若头结点为空则为空链表,否则不是。 * @return {Boolean} true:链表为空; false:链表非空。 */ if (this.head.next == null) { return true; } else { return false; } } top() { /** * 获取链表头结点的数据域内容 */ return this.head.next.data; } top_node() { /** * 获取链表头结点 */ var node = this.head.next; node.next = null; return node; } clear() { /** * 清空链表,只要把头结点干废,整个链表直接就清空了 */ this.head.next = null; this.length = 0; } append(elm) { /** * 用数据构成新结点插入单链表尾部 * @param elm {any} 需要插入链表尾部的新结点 */ if (this.head.next == null) { // 没有头结点 this.head.next = new Lnode(elm); this.length = this.length + 1; } else { var pointer = this.head.next; // 指向头节点 while (pointer.next != null) { pointer = pointer.next; }; pointer.next = new Lnode(elm); } } append_node(node) { /** * 将新结点挂载到链表尾部 * @param node {Lnode} 需要插入链表尾部的新结点 */ if (this.head.next == null) { // 没有头结点 this.head.next = node; this.length = this.length + 1; } else { var pointer = this.head.next; // 指向头节点 while (pointer.next != null) { pointer = pointer.next; }; pointer.next = node; }; } append_left(elm) { /** * 用数据构成新结点插入单链表头部 * @param elm {any} 需要插入链表尾部的新结点 */ if (this.head == null) { this.head.next = new Lnode(elm); // 若为空链表,直接将链表头设置为该结点 this.length = this.length + 1; // 增加结点长度 } else { // 先将新结点的`next`指向原第一个结点 var node = new Lnode(elm); node.next = this.head.next; // 再将`head`指向新的结点 this.head.next = node; this.length = this.length + 1; // 增加结点长度 }; } append_node_left(node) { /** * 将一个新结点插入到链表首部 * @param node {Lnode} 要从左侧也就是链头插入的新结点 */ if (this.head.next == null) { this.head.next = node; // 若为空链表,直接将链表头设置为该结点 this.length = this.length + 1; // 增加结点长度 } else { // 先将新结点的`next`指向原第一个结点 node.next = this.head.next; // 再将`head`指向新的结点 this.head.next = node; this.length = this.length + 1; // 增加结点长度 }; } get_node(index) { /** * 获取索引号为`index`的结点。 * 索引号是从数字`0`开始计算的 * @param index {number} 索引号 * @return node {Lnode} 索引得到地节点 */ if (index < 0) { throw "ValueError: Index must be a positive integer!"; } // 负数索引报错 else if (index + 1 > this.length) { throw "ValueError: Index exceeds the maximum value!"; } // 索引过界报错 else { var pointer = this.head.next; // 从头结点开始 for (var i = 0; i < index; i++) { pointer = pointer.next; // 逐个指向下一个结点 }; pointer.next = null; // 斩断后继 return pointer; }; } get(index) { /** * 索引结点地数据 * @param index {number} 索引号 * @return data {any} 链表中与索引号对应地节点地数据域内容 */ return this.get_node(index).data; } index(x, strict) { if (strict === void 0) { strict = true; } /** * 返回链表中第一个数据域 x 的元素的零基索引 * @param x {any} 用于寻找索引的数据 * @param strict? {boolean} 是否开启严格模式,如果不开启严格模式在统计时相同的不同类型 */ var pointer = this.head; var index = 0; // 当前考察结点的索引号,从0开始计算(所谓零基) if (strict == true) { while (pointer.next) { if (pointer.next.data == x && typeof (pointer.next.data) == typeof (x)) { return index; } else { console.log(pointer.next.data, x); console.log(pointer.next.data == x, typeof (pointer.next.data) == typeof (x)); index++; pointer = pointer.next; } } } else { while (pointer.data != x) { index++; pointer = pointer.next; } } return index; } count(x, strict) { if (strict === void 0) { strict = true; } /** * 返回链表中结点的值域存储内容与x内容相同的结点的个数 * @param x {any} 用于比较的数据 * @param strict? {boolean} 是否开启严格模式,如果不开启严格模式在统计时相同的不同类型 * 可能同时被统计,如数字 1 和字符串 "1" * @returns counter {number} 被统计对象在链表数据域中出现的次数 */ var counter = 0; var pointer = this.head.next; while (pointer) { if (pointer.data == x) { if (!strict) { // 不严格判断类型模式 counter++; } else { // 严格判断类型模式 if (typeof (pointer.data) == typeof (x)) { counter++; } } }; pointer = pointer.next; }; return counter; } insert(index, node) { /** * 在链表的第`index`个节的位置挂载新的结点`node`,其中从结点为`index = 0`开始编号 * 也就是说,新的结点索引号将为`index`,而这个结点将挂载到索引号为`index-1`的结点后面 * * @param index {number} 新结点插入的索引号 * @param node {Lnode} 要插入的新结点 */ if (node != null && node.next == null) { node.next = null; }; // 只允许插入单个结点,若有后继直接切断 if (index == 0) { node.next = this.head; // 先收小弟 this.head = node; // 再上位 } else { var pointer = this.head; // 从头结点开始 if (index == 0) { this.append_node_left(node); } else { for (var i = 0; i < index - 1; i++) { // 调整指针所指,即调整引用对象位置 pointer = pointer.next; // 逐个指向下一个结点,直至`pointer`指向被索引结点的前一个结点 } node.next = pointer.next; // 先收小弟 pointer.next = node; // 再上位 this.length = this.length + 1; // 更新结点长度 }; }; } to_array_node() { /** * 获取链表结点依次构成的数组 * @returns elem {Lnode[]} 以此装载了被遍历到的所有结点(这里其中每个结点都是孤立、干掉next的) */ var elm = []; // 准备好用于容纳所有的结点 var pointer = this.head.next; // 挂载操作一开始时指针指向头部结点、 if (pointer == null) { // 空链表,不仅要返回一个空数组,还要抛出提示 console.warn("Warning: It seems that you hava traversed an empty Linked List!"); return elm; } else { // 非空链表 while (pointer.next != null) { // 存在下一个结点时 if (pointer == null) { // 停止(拦截)条件:(直到)结点为`null` break; } else { // 获取当前结点剔除链接关系的`孤立结点`挂载结点数组 elm.push(new Lnode(pointer.data)); }; pointer = pointer.next; // 指向后继 }; elm.push(new Lnode(pointer.data)); // 最后一个元素 return elm; }; } to_array() { /** * 获取链表结点依次构成的数组 * @returns elem {Lnode[]} 以此装载了被遍历到的所有结点(这里其中每个结点都是孤立、干掉next的) */ var elm = []; // 准备好用于容纳所有的结点 var pointer = this.head.next; // 挂载操作一开始时指针指向头部结点、 if (pointer == null) { // 空链表,不仅要返回一个空数组,还要抛出提示 // console.warn("Warning: It seems that you hava traversed an empty Linked List!"); return elm; } else { // 非空链表 while (pointer.next != null) { // 存在下一个结点时 if (pointer == null) { // 停止(拦截)条件:(直到)结点为`null` break; } else { // 获取当前结点剔除链接关系的`孤立结点`挂载结点数组 elm.push(pointer.data); }; pointer = pointer.next; // 指向后继 }; elm.push(pointer.data); // 最后一个元素 return elm; }; } replace(index, data) { /** * 替换指定索引处结点的数据 * 该方法不会改变结点的连接关系 */ if (index < 0) { throw "ValueError: Index must be a positive integer!"; } else if (index + 1 > this.length) { throw "ValueError: Index exceeds the maximum value!"; } var pointer = this.head; var ct = 0; while (ct < index) { // 指针逐渐指向被修改数据的前一结点 pointer = pointer.next; ct++; }; pointer.next.data = data; // 修改结点的数据域 } replace_node(index, node) { /** * 替换指定索引处的结点 */ if (index < 0) { throw "ValueError: Index must be a positive integer!"; } else if (index + 1 > this.length) { throw "ValueError: Index exceeds the maximum value!"; } var pointer = this.head; var ct = 0; while (ct < index) { pointer = pointer.next; ct++; }; node.next = pointer.next.next; pointer.next = node; } drop(index1, index2) { if (this.head == null) { throw "EelEmptyListError: Unable to pop up node from an empty linked list."; } else { if (index2 == undefined) { /** * 给定一个索引号`index1`时 * 删除索引号为`index1`的某个结点及其之后的所有结点 * @param index {number} 结点索引号,该索引号指示结点及其之后全部结点都将从链表中删除 */ if (index1 < 0) { throw "ValueError: Index must be a positive integer!"; } else if (index1 + 1 > this.length) { throw "ValueError: Index exceeds the maximum value!"; } if (index1 == 0) { this.clear(); } else { var pointer = this.head.next; for (var i = 0; i < index1 - 1; i++) { pointer = pointer.next; // 指向下一个,直至指向索引结点的前一个 }; pointer.next = null; // 通过清空被索引结点的前一个结点的后继指针,将被索引系欸但及其后从链表删除 }; } else { /** * 给定两个索引号`index1`和`index2`时 * 删除索引号从`a`(包含)到`b`(包含)的一段结点 * 允许`a`和`b`任意一个索引值更大,即不区分区间起始大小。 * @param index {number[]} 包含两个索引值范围的数组。该范围内的所有结点都将从链表中删除。 */ // if(index1==index2){this.del(index1)} if (index1 == index2) { this.del(index1); } else if (index1 + 1 > this.length || index2 + 1 > this.length) { throw "ValueError: Index exceeds the maximum value!"; } else if (index1 < 0 || index2 < 0) { throw "ValueError: Index must be a positive integer!"; } else { var a = index1; var b = index2; if (a > b) { // 不论先前哪一个索引值更大,始终转换为a更小、b更大 a = index2; b = index1; } var pointer = this.head; // 引用头节点 if (a != 0) { for (var i = 0; i < a; i++) { // 处理结点范围为:[0:a-1] pointer = pointer.next; // 以`pointer`为指针,逐步指向下一个结点,直至指向索引a结点的前一个结点 }; for (var i = 0; i < b - a + 1; i++) { // 处理结点范围为:[a:b] pointer.next = pointer.next.next; // 以`pointer.next`为指针,逐步指向下一个结点,直到跳过中间`b-a+1`个结点 }; // 到此为止,后面[b+1,]的结点也是自动挂着的,无需再处理 } else { for (var i = 0; i < b + 1; i++) { // 处理结点范围为:[a:b] pointer.next = pointer.next.next; // 以`pointer.next`为指针,逐步指向下一个结点,直到跳过中间`b-a+1`个结点 }; // 到此为止,后面[b+1,]的结点也是自动挂着的,无需再处理 } } } } } del(index) { /** * 删除索引号为`index`的某个结点,保留其后继结点 * @param index {number} 将被删除的结点的索引号 */ if (index < 0) { throw "ValueError: Index must be a positive integer!"; } else if (this.head.next == null) { throw "EelEmptyListError: Unable to pop up node from an empty linked list."; } else if (index + 1 > this.length) { throw "ValueError: Index exceeds the maximum value!"; } else { if (index == 0) { this.pop_left(); } else { var pointer = this.head.next; // 创建指针(对象引用) for (var i = 0; i < index - 1; i++) { pointer = pointer.next; // 指向下一个,直至指向被索引结点的前一个结点 }; pointer.next = pointer.next.next; // 跳过被索引结点以实现删除之 }; }; } pop() { /** * 弹出最右边的一个结点并返回 * @returns node {Lnode} 原链表最右侧的一个结点 */ if (this.head.next == null) { throw "PopEmptyListError: Unable to pop up node from an empty linked list."; } else if (this.length == 1) { var last = this.head.next; this.head.next = null; return last; } else { var pointer = this.head; // 创建指针(对象引用) while (pointer.next.next != null) { pointer = pointer.next; // 指向下一个,直至指向倒数第二个结点 }; var last = pointer.next; pointer.next = null; this.length = this.length - 1; if (this.length == 0) { this.head = null; }; return last; }; } pop_left() { /** * 弹出最左边的一个结点并返回 * @returns node {Lnode} 原链表最左侧的一个结点 */ if (this.head.next == null) { throw "PopEmptyError: Unable to pop up node from an empty linked list."; } else if (this.length == 1) { var last = this.head.next; this.head.next = null; return last; } else { var pointer = this.head.next; this.head.next = this.head.next.next; // 头结点右移一个 pointer.next = null; // 斩断后继联系 this.length = this.length - 1; // 更新链表长度 if (this.length == 0) { this.head = null; }; return pointer; } } merge(list) { /** * 实现两个链表的合并 * 传入另外一个链表,将这个新传入的链表挂载到本链表的后边 * @param list{LinkList} 将要合并到本链表的一个新链表 */ var pointer = this.head; // 指向本链头链表指针,实际上是对链表结点对象的引用 while (pointer.next != null) { // 先将指针移动到本链表的尾部 pointer = pointer.next; }; pointer.next = list.head; // 将本链表的链尾结点指向新链表的链头结点 } reverse() { /** * 将原链表就地反转,不反返回何结果 * 该方法将会改变原链表 */ var pointer = this.head; // 引用头指针 var h = new Lnode(); // 新头指针 while (pointer.next) { // 若原链表还有结点 var node = this.pop_left(); // 原链表弹出第一个结点 if (h.empty) { // 空 h.next = node; h.empty = false; } else { node.next = h.next; h.next = node; }; }; this.head = h; } get_reverse() { /** * 反转链表 * 原链表不变,返回反转后的新链表 */ var pointer = this.head; // 引用头指针 var list = new LinkList(); // 一个新链表 while (pointer.next) { // 逐个元素出来 var node = this.pop_left(); list.append_left(node.data); }; return list; } reverse_by_k(k) { /** * 将链表中的所有结点以k个为一组进行翻转 * @param k{number}分组的结点元素个数 */ var pointer = this.head; // 引用头指针 var h = new Lnode(); // 新头指针 var chain_p = new Lnode(); // 用来引用每k个结点 var ct = 0; // 计数器 while (pointer.next) { // 若原链表还有结点 if (ct < k) { // 还不够k个结点,则增长子节点链 var node = this.pop_left(); // 原链表弹出第一个结点 if (chain_p.empty) { chain_p.next = node; chain_p.empty = false; } else { node.next = chain_p.next; chain_p.next = node; }; ct++; if (this.length == 0) { // 针对最后一段的挂载 var pt = h; while (pt.next) { pt = pt.next; }; pt.next = chain_p.next; }; } else { // 已经够k个结点,则将子结点挂载到新头,清空子结点链与计数器 if (h.empty) { h.next = chain_p.next; h.empty = false; } else { var pt = h; while (pt.next) { // 逐渐指向新子链表最后一个结点 pt = pt.next; } pt.next = chain_p.next; }; chain_p = new Lnode(); ct = 0; }; }; this.head = h; } };