本篇教程由作者设定使用 CC BY-NC-SA 协议。
前言
JavaScript (简称 JS)是现代浏览器通用的脚本语言,在 LatvianModder 的 KubeJS 模组中,JS 还能用来魔改 Minecraft!如果你想使用这个模组,你就需要了解 JavaScript。
第一行代码
安装 KJS 启动一次游戏之后你会在游戏目录发现一个叫做 kubejs 的文件夹,现在进入 kubejs/server_scripts/,里面有一个 script.js 文件,你可以用编辑器(个人推荐使用 Visual Studio Code)打开它然后清空所有内容来写你的代码,或者自己新建一个 js 文件。
现在把下面的代码写进这个 js 文件:
console.log('Hello World!')
保存,然后启动游戏,如果你已经在游戏中就使用 /reload 指令,如果没有报错,不出意外你会在日志文件中看到一行 "Hello World!"(当然它可能淹没在了众多其他信息中)。
JS 的代码不必写分号,这很厉害!
基本数据类型
JavaScript 有 8 种基本数据类型,其中有 7 种原始类型:
- 数字(Number)
- 字符串(String)
- 空(Null)
- 未定义(Undefined)
- 布尔(Boolean)
- BigInt
- Symbol
以及一种引用类型:
- 对象(Object)
BigInt 和 Symbol 类型在 KJS 中几乎不可能会用到,本文不作介绍,有兴趣的读者可以自己查询相关信息。
Number 类型不像有些其他语言那样分整数和浮点数,JS 中的 Number 是符合 IEEE 754 标准的 64 位双精度数字。
JavaScript 变量
以下的代码定义一个变量。
var a = 1
// 或者
let a = 1
JS 是一个动态类型语言,你不需要在定义变量时指定数据类型,解释器会自动推断类型(有时候还会进行隐式转换)。
var 和 let 的区别在于,var 定义的变量可以跨块访问,而 let 定义的变量只能在当前的块访问。相同点是它们都不能跨函数访问。
此外你还可以使用 const 来声明一个常量。
const a = 1
const 和 let 的作用域相同,但是 const 声明的常量的值不能通过重新赋值改变,在声明时也必须指定初始值,也不能重新使用 const 声明已有的常量。
JavaScript 运算符
数学运算符:
运算符 | 作用 | 运算符 | 作用 |
---|---|---|---|
+ | 加法 | += | 加法并赋值 |
- | 减法 | -= | 减法并赋值 |
* | 乘法 | *= | 乘法并赋值 |
** | 幂运算 | **= | 幂运算并赋值 |
/ | 除法 | /= | 除法并赋值 |
% | 取余 | %= | 取余并赋值 |
逻辑运算符:
运算符 | 作用 |
---|---|
&& | 且 |
|| | 或 |
! | 非 |
其他运算符(我不知道怎么归类):
运算符 | 作用 |
---|---|
= | 赋值 |
== | 相等 |
!= | 不相等 |
=== | 严格相等 |
!== | 不严格相等 |
5 == '5' // true
5 === '5' // false
JavaScript 对象
以下代码创建一个对象:
let obj = {
a: 1,
b: 2
}
对象是一种数据类型,以键值对的方式存储数据,在上面的代码中,a、b是键,1、2 是值。
你可以使用如下方式访问对象中的属性:
let a = obj.a
// 或者
let a = obj['a']
对象除了属性也可以有方法,方法简单来说就是对象中的函数。
以下代码在 obj 中声明一个名为 hello 的方法:
let obj = {
a: 1,
hello: () => {
console.log('Hello!')
}
}
// 或者
let obj = {
a: 1,
hello(){
console.log('Hello!')
}
}
// 或者
let obj = {
a: 1,
hello: function(){
console.log('Hello!')
}
}
// 或者
function helloWorld() {
console.log('Hello!')
}
let obj = {
a: 1,
hello: helloWorld
}
容易看出 JavaScript 是一个非常灵活的语言。
要调用方法,就这样做:
obj.hello() //要有括号,否则只是访问这个方法的内容而不是调用方法
// 或者
obj['hello']() // 这样也行,不过我从没见过有人这样做
显然方法也只是一种特殊的属性,毕竟函数也只是特殊的对象(后面会提到)。
JavaScript 数组
数组(Array)是一种特殊的对象。
以下代码创建一个数组:
let arr = [1, 2, 3]
这样做本质上是创建了一个 Array 对象,和以下的代码等价:
let arr = new Array(1, 2, 3)
访问一个数组中的某个元素:
let a = arr[0]
数组的下标从 0 开始,注意,虽然数组也是对象,但不能使用 . 访问数组中的元素。
使用 .length 原型属性来获取数组的元素数量,这在写循环时非常有用。
数组有一些原型方法,在 KubeJS 中最常用的是 .foreach():
let items = [
'minecraft:apple',
'minecraft:carrot',
'minecraft:potato'
]
items.foreach(item => {
console.log('I love ' + item + ' !')
})
输出是这样:
I love minecraft:apple !
I love minecraft:carrot !
I love minecraft:potato !
.foreach() 接受一个回调函数作为参数,并将数组中的每一项都作为参数传给这个回调函数。
如果你想要结合两个或更多数组,使用 .concat() 方法:
let arr1 = [1, 2, 3]
let arr2 = [4, 5, 6]
let arr3 = arr1.concat(arr2) // [1, 2, 3, 4, 5, 6]
以及一些常用的原型方法:
方法名 | 参数 | 作用 |
---|---|---|
.push() | 一个或多个元素 | 将传入的元素插入数组后 |
.includes() | 一个元素 | 如果传入的元素存在,返回 true,否则返回 false |
.sort() | (可选)排序函数 | 排序数组 |
.unshift() | 一个或多个元素 | 将传入的元素插入数组前 |
.pop() | 无 | 删除数组最后的元素并返回这个元素 |
.shift() | 无 | 删除数组第一个元素并返回这个元素 |
JavaScript 字符串
以下代码声明一个字符串:
let str = 'hello'
JS 中的字符串可以使用单引号或者双引号包裹。
字符串和数组有一些相似之处,你可以使用下面的方法访问字符串中的某个字符:
let a = str[0]
或者使用 .charAt() 原型方法:
let a = str.chatAt(0)
如果要拼接两个字符串,只需要使用 + 号:
let str1 = 'hello'
let str2 = ' world'
let str3 = str1 + str2 // 'hello world'
当然也可以使用 .concat() 方法,但是比较麻烦。
还有一种特殊的字符串叫做模板字符串,使用反引号(`)包裹:
let arr = `Hello
World!`
模板字符串可以是多行的,另外还支持插值:
let a = 1
let b = 2
console.log(`a is ${a}, b is ${b} !`) // a is 1, b is 2 !
在字符串中使用反斜杠(\)来转义字符:
`\${}` == '${}' //true
支持 Unicode 编码:
'\u4f60\u597d' == '你好' //true
JavaScript 循环与条件判断
JS 中有几种循环的方式,最常用的是 for 循环:
for(let i = 0; i < 10; i++){
console.log(i)
}
可以发现 for 循环的语法和 C 语言一样,以上代码会在控制台中依次输出0-9。
如果你有一个数组,除了 .forEach() 方法外,也有方便的方法遍历数组中的每一个元素:
let arr = [1, 2, 3, 4]
for(let i in arr){
console.log(i)
}
如果你想中断循环,使用 break:
for(let i = 0; i < 10; i++){
if(i == 5) break;
console.log(i)
}
你会发现控制台输出 4 之后就结束了。
if 语句用于条件判断,括号中的表达式如果是真,就执行 if 后面的代码,如果为假,就跳过。
布尔值 false,数字 0,undefined,null 均被视为假,其余任何值(或者表达式)都视为真。
JavaScript 函数
有两种方法声明函数:
// 1
function sayHi1() {
console.log('Hi')
}
// 2
let sayHi2 = () => {
console.log('Hi')
}
后一种方法声明的函数称为箭头函数,如果箭头函数只有一个参数,可以省略括号,如果函数体只有一行代码,可以省略花括号。
两种方法声明的函数直接调用没有什么区别,但是箭头函数没有自己的 this,.bind() 原型方法也对箭头函数无效。
函数名不是必要的,没有函数名的函数叫做匿名函数。KJS 中存在许多需要回调函数的地方,这些回调函数绝大多数都是匿名函数。
JS 中的函数也是对象,你可以轻松地通过 = 来拷贝一个函数。
函数需要一个返回值:
function foo(){
return 'foo!'
}
如果没有 return 语句,或者 return 后面没有值,则返回 undefined。
return 除了返回值之外,还能跳出函数回到上一级的上下文中:
function foo(arg){
if(arg == 'bar') return;
console.log('foo!')
}
Function.prototype.bind()
我在使用 KJS 给物品和方块添加标签的时候发现,类似 forge:ores/iron 这样的标签被添加到目标时,父级标签 forge:ores 不会被同时添加,而且也不支持给一个目标同时添加多个标签,这就意味着我得再写更多代码,我很不高兴,于是写了一个函数:
function splitTag(tag){
let tagList = []
let splitedTag = tag.split('/')
tagList.push(splitedTag[0])
for(let i = 1; i< splitedTag.length; i++) {
tagList.push(tagList[i-1] + '/' + splitedTag[i])
}
return tagList
}
function advancedAdd(tags, target){
function temp_addTag(tag){
if(tag.includes('/')){
let tagList = splitTag(tag)
tagList.forEach(singleTag => {
this.add(singleTag, target)
})
}else this.add(tag, target)
}
let addTag = temp_addTag.bind(this)
if(typeof tags == 'string'){
addTag(tags)
}else{
tags.forEach(tag => {
addTag(tag)
})
}
}
在此就不解释这个函数的工作方式了,你只需要知道面对 forge:ores/iron 这样带有斜杠的标签的时候,advancedAdd() 会依次将父级标签添加到目标上(在这个例子中是 forge:ores);面对多个标签的时候,也能依次添加到目标上。
然而问题出现了,.add() 是 KubeJS 中 TagEventJS 对象自己的方法,你不能在这个对象之外的地方使用它,也就是说:
// event 的类型是 TagEventJS
onEvent('item.tags', event => {
// 我只能把函数声明在这里
})
本来这是一个非常通用的函数,但是现在我要在每一个 js 文件里面都声明一遍这个函数,这很坏,而且很不优雅!
幸运的是,Function.prototype.bind() 原型方法可以创建一个函数的拷贝,这个新函数的 this 被绑定到了 .bind() 的参数上。
具体来说,我可以:
onEvent('item.tags', event => {
let advAdd = (tags, target) => advancedAdd.bind(event)(tags.target)
})
这样就创建了一个名为 advAdd 的函数,功能与 advancedAdd 完全一致,但是由于 this 被绑定到了 event 上,所以可以正常使用 event.add() 方法,函数的声明也可以放在其他地方供其他 js 文件使用!
JavaScript 面向对象与原型链
我在修改匠魂材料属性的时候为了方便,写了下面的代码:
function Traits(){
this.default = []
}
Traits.prototype.add = function (name, level){
level = level || 1
this.default.push(
{
name: name,
level: level
}
)
return this
}
这种函数叫做构造函数,它创建了一个名为 Traits 的类(本质上还是对象),Traits 类拥有一个叫做 .add() 的原型方法。
接下来试试使用它(可以直接在浏览器控制台里复制粘贴上面的代码和下面的代码,毕竟只是为了演示):
let myTrait = new Traits()
console.log(myTrait)
如果你是在浏览器控制台里面执行的代码,你会看到这个:
Traits {default: Array(0)}
这意味着创建了一个 Traits 对象,其中有个叫 default 的属性,值是一个空数组。
接下来试试 .add() 方法:
myTrait.add('tconstruct:momentum', 2)
console.log(myTrait)
现在可以看到 default 里面的数组不是空的了,从源代码可以看出 .add() 方法是就是往 default 里面装东西。
神奇的是,你可以不止一次地调用 .add():
myTrait.add('tconstruct:overslime', 2).add('tconstruct:insatiable', 1)
console.log(myTrait)
为什么?因为 .add() 除了往 default 里面装东西,还返回了 this。在 Traits 对象中,this 指向的就是它自己,因此 .add() 返回的是一个 Traits 对象,而 Traits 对象拥有 .add() 方法,它又返回一个 Traits 对象……你可以一直这样做,这叫链式调用。
读到这里相信你对 JS 中的原型有了一点感性的认识,JS 中的每个对象都有一个名为 prototype (原型)的属性,原型也有自己的原型,这样的继承关系一直持续下去,最后指向 null,这就是原型链。
在 JS 中可以直接使用 class 来创建类:
class Traits{
constructor() {
this.default = []
}
add(name, level){
level = level || 1
this.default.push(
{
name: name,
level: level
}
)
return this
}
}
constructor() 方法在类初始化时调用,也就是构造函数。
这种方式本质上就是上文那种方式的语法糖,可惜的是 KJS 不支持 class 声明。