前言
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 = 1JS 是一个动态类型语言,你不需要在定义变量时指定数据类型,解释器会自动推断类型(有时候还会进行隐式转换)。
var 和 let 的区别在于,var 定义的变量可以跨块访问,而 let 定义的变量只能在当前的块访问。相同点是它们都不能跨函数访问。
此外你还可以使用 const 来声明一个常量。
const a = 1const 和 let 的作用域相同,但是 const 声明的常量的值不能通过重新赋值改变,在声明时也必须指定初始值,也不能重新使用 const 声明已有的常量。
JavaScript 运算符
数学运算符:
运算符作用运算符作用+加法+=加法并赋值-减法-=减法并赋值*乘法*=乘法并赋值**幂运算**=幂运算并赋值/除法/=除法并赋值%取余%=取余并赋值逻辑运算符:
运算符作用&&且||或!非其他运算符(我不知道怎么归类):
运算符作用=赋值==相等!=不相等===严格相等!==不严格相等5 == '5' // true5 === '5' // falseJavaScript 对象
以下代码创建一个对象:
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 = 1let b = 2console.log(`a is ${a}, b is ${b} !`) // a is 1, b is 2 !在字符串中使用反斜杠(\)来转义字符:
`\${}` == '${}' //true支持 Unicode 编码:
'\u4f60\u597d' == '你好' //trueJavaScript 循环与条件判断
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 函数
有两种方法声明函数:
// 1function 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 的类型是 TagEventJSonEvent('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 声明。