|
因为 QuickJS 这样的东西没有早出来几年,否则根本没有 Lua 什么事情,归根揭底,Lua 并不是一门好语言:
- 作用域默认是 global 的,不是 local 的,但凡最近三十年发明的语言,变量和函数定义基本都是默认 local 的作用域 ,lua 这种默认 global 的设计,强迫你到处写满 local,简直是一口气直追 50 年前的古圣先贤。
- 索引从 1 开始:记忆里只有 Pascal / QBasic 是这么做的,但 pascal 事实上允许索引从任意位置开始(从 0 / 1 / 100 开始都可以)。
- 到处是 nil,你的代码四处和 nil 作斗争,明明可以有更优雅的机制的,却什么都用 nil。
- 到现在都没有 unicode 支持,字符串是 bytes 的别名。
- 到现在都没有 switch/case,只能写几十行 if/else,隔壁 python 都能模式匹配了。
- 到现在都没有 type hint,隔壁的 python 在 7 年前就有 type hint 和 type check 了。
- 项目更新速度异常缓慢,最近十年尤其如此,作者以出世的态度做入世的项目。
- 前几个版本 table 长度好像还要自己数,现在不用了,但至今打印个 table 内容都没标准方法
- 至少有 5 种方法定义一个模块,3 种方法定义一个类。
- 缺乏基础库,每个项目都重新发明了一套,导致你的脚本很难跨项目复用。
- 一个项目里的代码基本很难不做修改的给第二个项目用,知识无法积累。
- 缺乏妥善的周边工具。
- 更多的选择意味着更多的迷惑与更多的束缚,选择多看似更好,但往往最终带来更多痛苦。
明明是 90 年代才发明的语言,浑身透着一股 60-70 年代的味道。
那么使用 QuickJS 代替 lua 的有哪些好处呢?
- QuickJS 同 Lua 一样小巧,代码就几个文件,运行只需要 200KB 的内存就能跑个简单程序。
- QuickJS 遵从 ES2020 标准,可以跑全部 ES2020 测试用例,也能轻松支持 TypeScript。
- 基于 JS 的技术栈有丰富的前人成果供你使用,生态更好。
- JavaScript 的人员很容易招聘。
- 少打字: { } vs begin end 。
- JavaScript 有 Uint8Array, Float32Array 等内建类型,能比 Lua 更高效的处理媒体数据。
- 简单逻辑直接用 JavaScript 撸,复杂业务可以上优雅又安全的 TypeScript 。
- 逻辑可以复用到 Web / Electron 上,向 web 迁移容易很多。
- QuickJS 在众多 JS 虚拟机实现里,性能是比较好的,V8 下来就是它了。
- Lua 在 github 上只有 1 万个开源项目,很多还是用 Lua 的宿主项目和 neovim 配置,非纯 Lua 项目,你复用不了。
- JavsScript 在 github 上有 38 万个项目,大部分是可以用被你复用的纯 js 项目。
- TypeScript 短短几年,在 github 上就有 13 万个项目了。
- 团队在 JavaScript 上积累的知识可复用到:移动应用,桌面应用和 web 开发,不光做游戏。
- JS/TS 有很多优秀的开发环境和丰富的周边工具。
事实上周边一些中型引擎最近两年都完成了 QuickJS 的集成,用它逐步替代 Lua,架不住 js 的人好招聘,技术生态好,架不住 js 还可以有 ts 的加持。
所以说,QuickJS 要是早出来几年,根本没 Lua 什么事情了,老项目选 Lua 是没办法,新项目可以多看看,多比较下,没必要看着继续在 Lua 上面继续浪费时间。
--
没有对比就没有伤害,TypeScript 写个最简单的程序,定义个类:
多清爽,简单直白,程序不就应该这样写吗?使用也是直接:
var p = new Person("skywind", 18, 1800)
console.log(p.toString())
Lua 里所谓 less is more,就是不给你提供个 class,告诉你可以用 table + setmetatable 模拟:
Person = {}
Person.__index = Person
function Person:create(name, age, salary)
local obj = {}
setmetatable(obj, Person)
obj.name = name
obj.age = age
obj.salary = salary
return obj
end
function Person:toString()
return self.name .. ' (' .. self.age .. ') (' .. self.salary .. ')'
end
local p = Person:create('skywind', 18, 1800)
print(p:toString())你摸着良心说说你想写哪种代码?Lua 这种类定义,一眼看过去真的是一团乱麻,这还不算最恶心的,等你开始写继承的时候才恶心,还有那个 : 符号和主流语言格格不入。
这种程序写大了是很容易一团乱麻的,容易写飞,你忘记写一行 Person.__index = Person 你可能怎么死的都不知道,很多新技术最初的目标都在于简化现有技术,但是最终却以增加了更多额外的复杂性收场,其他语言直接定义个 class 这么简单基础的事情,在 lua 里都那么恶心。
有的项目为了简化这件事情,实现了叫做 class 的函数,让你可以这么定义一个类:
Account = class(function(acc,balance)
acc.balance = balance
end)
function Account:withdraw(amount)
self.balance = self.balance - amount
end
-- can create an Account using call notation!
acc = Account(1000)
acc:withdraw(100)好看么?不好看,但它帮你处理好了 setmetatable,继承等等琐事,避免遗漏,可这个项目实现的 class 和别的项目实现的 class 又不兼容,互相不能继承不说,就连实例化都可以有 n 种方法:
p = Person:create("project1", 18, 1800)
p = Person:new("project2", 18, 1800)
p = Person("project3", 18, 1800)项目 1 和项目 2 的实例化函数(构造)名字用的不一样,项目3 自己实现了 class,直接调用类名,第一个项目实现的类,按第二个项目的方法无法调用。
没有标准就无法协同,就连外面的 Editor/IDE 都很难得知你定义了个什么类,或者类里有些什么成员,因此无法在开发时给予你更多的帮助和支持。
写多了你会问自己,为什么要在 class 定义兼容性这种莫名其妙的事情上浪费这么多时间呢?为什么不能像 ts/js 一样所有项目统一用 new 实例化,用 class 关键字声明呢?
这时候 Lua 会告诉你:“这叫做 Less is more 原则,你不懂”,是不是听了很想砸键盘?
正是由于 Lua 的残缺,导致了语言碎片化,每个项目都要在一些很根本的东西上自己搞一套,导致项目之间代码很难复用,导致知识无法积累,大部分 lua 代码很难脱离所在项目,独立存在,知识积累不起来的后果就是 Lua 虽比 TypeScript 早 21 年,开源项目却不到 TypeScript 的 1/10 。
翻过去看看前面的 TypeScript 对比下,谁美谁丑?谁好谁坏?程序写大了谁的更容易维护?一目了然的事情;TypeScript 还能在编译期就帮你在 Editor 里把错误标出来:
vscode 里都不需要编译,边写边实时标注错误,用 Lua 的话,非要运行到那里,你才能得知出错了。这就是 Lua 的 "Less is more" ,不告诉你错哪里了,让你自己运行时自己踩地雷去,最终你花费了更多(more)的时间为它的残缺(less)买单。
所以说 Lua 不适合写大程序,程序大了 Lua 代码容易写散,容易失去维护性。
Lua 有个很迷惑人的话术,叫做:“Lua 的定位从来就是小而精,不是 Python/Java”,你要真的只用 Lua 写一些一两百行的小代码,配置之类的,我也无话可说,但游戏开发里动辄用 Lua 写几万行的新模块,不得不面对复杂性和可维护性时,就不要再用 “小而精的定位” 当成它语法残缺的挡箭牌了。
--
再给你们欣赏下,如果 Lua 想不依赖任何库,怎么检测一个文件是否存在:
-----------------------------------------------------------------------
-- file or directory exists
-----------------------------------------------------------------------
function os.path.exists(name)
if name == '/' then
return true
end
if os.native and os.native.exists then
return os.native.exists(name)
end
local ok, err, code = os.rename(name, name)
if not ok then
if code == 13 or code == 17 then
return true
elseif code == 30 then
local f = io.open(name,"r")
if f ~= nil then
io.close(f)
return true
end
elseif name:sub(-1) == '/' and code == 20 and (not windows) then
local test = name .. '.'
ok, err, code = os.rename(test, test)
if code == 16 or code == 13 or code == 22 then
return true
end
end
return false
end
return true
end为了保证代码的 portable,不依赖 C 模块,只使用 Lua 标准 api 写出来就是这样,全是 work-around,再来欣赏下如何求一个文件的绝对路径:
-----------------------------------------------------------------------
-- absolute path (system call, can fall back to os.path.absolute)
-----------------------------------------------------------------------
function os.path.abspath(path)
if path == '' then path = '.' end
if os.native and os.native.GetFullPathName then
local test = os.native.GetFullPathName(path)
if test then return test end
end
if windows then
local script = 'FOR /f "delims=" %%i IN ("%s") DO @echo %%~fi'
local script = string.format(script, path)
local script = 'cmd.exe /C ' .. script .. ' 2> nul'
local output = os.call(script)
local test = output:gsub('%s$', '')
if test ~= nil and test ~= '' then
return test
end
else
local test = os.path.which('realpath')
if test ~= nil and test ~= '' then
test = os.call('realpath -s \'' .. path .. '\' 2> /dev/null')
if test ~= nil and test ~= '' then
return test
end
test = os.call('realpath \'' .. path .. '\' 2> /dev/null')
if test ~= nil and test ~= '' then
return test
end
end
local test = os.path.which('perl')
if test ~= nil and test ~= '' then
local s = 'perl -MCwd -e "print Cwd::realpath(\\$ARGV[0])" \'%s\''
local s = string.format(s, path)
test = os.call(s)
if test ~= nil and test ~= '' then
return test
end
end
for _, python in pairs({'python3', 'python2', 'python'}) do
local s = 'sys.stdout.write(os.path.abspath(sys.argv[1]))'
local s = '-c "import os, sys;' .. s .. '" \'' .. path .. '\''
local s = python .. ' ' .. s
local test = os.path.which(python)
if test ~= nil and test ~= '' then
test = os.call(s)
if test ~= nil and test ~= '' then
return test
end
end
end
end
return os.path.absolute(path)
end先尝试用 realpath ,不行再尝试用 perl 和 python 来求绝对路径 ,最后不行再自己根据当前目录重新计算。同时如果有 cffi 的话,会先尝试用 cffi 导出 msvcrt 的 GetFullPathName 来提供加速,都不行的话自己根据当前路径 join 计算。
爽吧?
不建议把时间浪费在 Lua 上,你在 Lua 上积累的三年经验,将来如果不做游戏就全废了,但如果你把经验积累在 js/ts 上,不做游戏你还可以做非常多的领域。
--
--
答疑:
1)Lua 没有 global 只有 env,说 global 不准确。
我用 global 只是个代称,具体指代 _G 还是 _ENV 看你用不用 luajit 或者 5.1,就是 “非 local ” 的意思,不用纠结,默认污染的是 global 可以理解成 “默认污染 _G 或者 _ENV”就行了。
2)Lua 的数组可以从 0 开始计数的。
看这里:韦易笑:别被忽悠了 Lua 数组真的也可以从 0 开始索引?
3)Lua 的优势是虚拟机非常小。
QuickJS 也非常小,代码就几个文件,运行内存只要 200KB,就能跑个简单小程序。
4)JS 也没好到哪里去,对比 Lua 的话。
ES6 标准以前,JS 也许和 Lua 差不多,但 ES6 以后,JS 完善了不少,Lua 已经没法比了;再说,JS 再不好也可以用 ts 加持,QuickJS 支持到 ES2020,轻松跑 ts。
5)Less is more
前面已经说过很多了,Less 可以但不要残缺。
--
目前已知使用 QuickJS 的项目有:
- Sciter - Multiplatform HTML/CSS/JavaScript UI Engine (好多大厂用它做桌面 UI)。
- 抖音的图形引擎(去年已经从支持 Lua 变成了同时支持 Lua 和 JS,目前使用 JS 为主)。
- 支付宝 Cube 项目:支付宝体验科技:Cube 小程序技术详解 | Cube 技术解读
- miniblink: 龙泉寺扫地僧:使用quickjs替换v8的chromium
- 北海渲染引擎:北海 (Kraken) v0.9 — 支持 QuickJS 首屏性能再提升 20%
- 腾讯某工作室,用 QuickJS 做游戏内 UI 系统,多个已上线游戏在用。
- 腾讯的:PuerTS 为 unity 和 unreal 提供 TypeScript 支持,底层可用 QuickJS 或 V8。
- Hummer - 移动端高性能跨端开发框架
知乎里也能找到很多人再用:
- ImWiki:在 Android 使用 QuickJS JavaScript 引擎教程
- 空童:Windows平台下QuickJS开发及相应C版DLL Module制作简介
- 空童:Windows和Linux平台动态链接库版QuickJS制作
- 空童:QuickJS高级玩法—javascript作为脚本嵌入C++
--
想在自己游戏里引入 JavaScript/TypeScript 和 QuickJS 的,不用从头弄,腾讯开源项目 PureTS 已经帮你们做好了,同时为 unity 和 unreal 增加 TypeScript/JavaScript 支持,并且底层可以随意切换 QuickJS 或者 v8:
GitHub - Tencent/puerts: 普洱TS!Write your game with TypeScript in UE or Unity. PuerTS can be pronounced as pu-erh TS
根本无需自己从头搞,可直接用 PureTS 或者参考它自己实现。看这势头,或许不出几年 JS/TS 将逐渐成为游戏项目的标配。
--
扩展阅读:
- 韦易笑:什么大多数编程语言中的数组都是从0开始计数的?
- 韦易笑:别被忽悠了 Lua 数组真的也可以从 0 开始索引?
--
。 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|