Skip to content

nvim dev lua for ts devs

ES edited this page Jul 16, 2026 · 1 revision

Lua 语法 —— 面向 TS 开发者

对标 TypeScript。只讲不同,基础编程概念不展开 Lua 版本:5.1(LuaJIT,Neovim 用的是这个)


1. 心智模型速查

维度 TypeScript / JS Lua (5.1 / LuaJIT)
类型系统 结构化强类型(编译时) 动态弱类型,运行时 type()
基本类型 number / string / bigint / boolean / null / undefined / symbol number / string / boolean / nil / function / table / userdata / thread
复合类型 Array / Object / Map / Set / class 只有 table(数组 + map 合体)
索引 0-based 1-based(除 nvim_* 部分 API,必须看文档)
语句结束 ; 可选 无分号
字符串拼接 + 或模板字符串 ..(双点)
取长度 .length #str / #tbl
相等 === / == == / ~=(意思是 !=)
空值 null + undefined 两种 只有 nil
真值 除 0/''/null/undefined/NaN/false 外都 truthy 只有 nilfalse 是 falsy0'' 都 truthy
默认值 a ?? b / a || b a or b(注意 0 和 '' 也会短路到 a)
三元 cond ? a : b cond and a or b(坑见下方)
self-op += -= &&= 无,只能写全:x = x + 1
块作用域 {} / let / const do...end / local(无 const)
变量声明 let/const/var local(不写就是全局,坑)
作用域 词法 + 块级 词法 + 块级
箭头函数 (x) => x + 1 function(x) return x + 1 end
解构 const {a, b} = o 无原生语法,靠多返回值 local a, b = f()
扩展 ...arr / {...o} table.unpack(t) / {table.unpack(t)}
可变参 (...args) function(...) + select('#', ...)
多返回值 返回 tuple / object 一等公民return a, b, c
continue continue —— 用 goto continue + ::continue::
try/catch try/catch/finally pcall(fn, args) / xpcall
class 语法糖 用 table + metatable 手写
继承 extends setmetatable(child, {__index = parent})
模块 import/export require('x') + return M
async async/await 协程 coroutine.* 或 callback
正则 完整 PCRE Lua pattern(弱)或 vim.regex / vim.re / vim.lpeg
字符串方法 str.split('.') vim.split(str, '.', {plain=true})(没内置 split)

2. 变量与作用域

local x = 1            -- 块作用域(等价 TS 的 let)
local y, z = 2, 3      -- 多重赋值
x = 10                 -- 无分号、无类型标注

g_bad = 'OOPS'         -- 不写 local 就是全局,污染 _G(尽量别)

-- Lua 没有 const。约定大写常量:
local MAX = 100

-- 块作用域
do
  local inner = 1
end
-- inner 在这里未定义

唯一地道的坑:忘写 local → 变量进 _G。lua-language-server 会标红


3. 基础类型

-- number(Neovim 使用 LuaJIT,通常按双精度浮点数理解)
local n = 42
local f = 3.14
local h = 0xFF        -- 十六进制
-- 整除 5.3+:10 // 3 == 3
-- 位运算 5.3+:10 & 3,Lua 5.1 用 bit.band(LuaJIT)

-- string
local s1 = 'single'
local s2 = "double"
local s3 = [[
  long string
  多行,原样保留
]]
local s4 = [==[ contains ]] inside ]==]  -- 可带 = 当分隔符

-- boolean
local t, f = true, false

-- nil(= JS undefined;没有 null)
local x = nil
x = nil         -- 给 table 字段赋 nil = 删除字段(重要)

-- function(一等公民)
local fn = function(x) return x * 2 end

-- table(唯一复合类型)
local arr = { 1, 2, 3 }
local obj = { name = 'foo', age = 1 }

-- thread(协程)、userdata(C 对象)少用

4. 运算符全表

类别 运算符 说明
算术 + - * / % 除法总是浮点;Lua 5.3+ 有 // 整除
字符串 .. 拼接
比较 == ~= < > <= >= 不等号是 ~= 不是 !=
逻辑 and or not 短路求值;返回操作数而非 true/false
长度 # #str 字节数,#arr 数组部分长度
位运算 & | ~ << >> Lua 5.3+;LuaJIT 用 require'bit' 模块

和 TS 最大的差异and/or 返回操作数

local x = nil or 'default'   -- 'default'
local y = 0 or 'default'     -- 0(!不是 'default')
local z = '' or 'default'    -- ''(!)

-- TS 的 ?? 默认值只对 null/undefined 生效
-- Lua 的 or 对所有 falsy 生效,但只有 nil/false 是 falsy,所以对 number/string 安全
-- 陷阱:boolean 默认值用 or 会错
local flag = opts.flag or true   -- 如果用户传 false 也会变 true
-- 正确写法:
local flag = opts.flag
if flag == nil then flag = true end

三元

local msg = ok and 'yes' or 'no'   -- cond ? 'yes' : 'no'
-- 坑:如果 'yes' 本身是 nil/false,就会走到 'no'
-- TS 的 ? : 是语法,Lua 的 and/or 是值计算

5. 控制流

-- if / elseif / else / end
if x > 0 then
  ...
elseif x == 0 then
  ...
else
  ...
end

-- while
while cond do
  ...
end

-- for 数值
for i = 1, 10 do ... end          -- 1 到 10 含两端
for i = 1, 10, 2 do ... end       -- 步长 2
for i = 10, 1, -1 do ... end      -- 倒序

-- for 泛型
for k, v in pairs(tbl) do ... end       -- 遍历所有 key(顺序不定)
for i, v in ipairs(arr) do ... end      -- 遍历数组部分(1..n,遇 nil 停)

-- repeat(= do while)
repeat
  ...
until cond

-- 没有 continue
for i = 1, 10 do
  if i % 2 == 0 then goto continue end
  print(i)
  ::continue::
end

-- break 和 TS 一样

6. 函数

-- 函数是一等公民
local function add(a, b) return a + b end
local add = function(a, b) return a + b end   -- 等价,但推荐前者(递归友好)

-- 可变参
local function sum(...)
  local args = { ... }             -- 收成 table
  local n = select('#', ...)       -- 精确参数个数(含 nil 占位)
  local total = 0
  for _, v in ipairs(args) do total = total + v end
  return total
end

-- 多返回值
local function pair() return 'a', 1 end
local k, v = pair()
local t = { pair() }              -- { 'a', 1 }
local only = (pair())             -- 'a'(加括号截断)

-- 方法调用糖::(自动传 self)
local obj = {
  name = 'foo',
  greet = function(self) print('hi', self.name) end,
}
obj:greet()                       -- 等价 obj.greet(obj)

-- 定义方法
function obj:bark() print(self.name) end   -- 等价 function obj.bark(self)

-- 闭包
local function make_counter()
  local n = 0
  return function() n = n + 1; return n end
end

7. Table —— Lua 唯一的复合类型

-- 构造
local empty = {}
local arr   = { 10, 20, 30 }                   -- 数组
local obj   = { name = 'foo', age = 1 }        -- map
local mix   = { 1, 2, name = 'foo' }           -- 两种混合
local dyn   = { ['key with space'] = 1, [1 + 1] = 'two' }  -- 动态 key

-- 访问
arr[1]              -- 10(注意 1-based)
obj.name            -- 'foo'
obj['name']         -- 同上
obj.missing         -- nil,不报错

-- 修改
obj.age = 2
obj.new = 'x'
obj.age = nil       -- 删除字段

-- 数组操作
table.insert(arr, 4)          -- push
table.insert(arr, 1, 0)       -- unshift(第 2 参是位置)
table.remove(arr)             -- pop
table.remove(arr, 1)          -- shift
#arr                          -- 长度(只对"紧凑"数组可靠,含 nil 洞不可靠)
table.concat(arr, ', ')       -- join

-- 遍历
for i, v in ipairs(arr) do ... end    -- 数组部分,顺序,遇 nil 停
for k, v in pairs(obj) do ... end     -- 所有 key,顺序不定

-- 浅克隆 / 合并
local copy = vim.deepcopy(t)          -- Neovim 扩展
local merged = vim.tbl_extend('force', t1, t2)
local deep = vim.tbl_deep_extend('force', t1, t2)

重点差异(和 TS Array / Object)

  • 数组和对象是同一个东西 —— Lua 里 {1,2,3} 实际是 {[1]=1, [2]=2, [3]=3}
  • # 不可靠 —— 中间有 nil 就会返回任意"边界"。尽量用 ipairs 或自己维护 length
  • 顺序 —— ipairs 数值部分保证顺序;pairs 对 map 部分不保证
  • 键可以是任意非 nil 值 —— 字符串、数字、table、function 都能当 key

8. 元表(metatable)—— 运算符重载 / 类 / 代理

Lua 面向对象的基础。类比 TS 的 Proxy + Symbol.*

local mt = {}

-- __index:字段找不到时的 fallback(继承的核心)
mt.__index = function(t, key) return 'default' end
--
mt.__index = parent_table         -- 直接指向父 table

-- __newindex:赋值时触发(用于只读 / 代理)
mt.__newindex = function(t, k, v) error('readonly') end

-- __call:表当函数用
mt.__call = function(self, ...) return self:method(...) end

-- __tostring:print(t) 时用
mt.__tostring = function(t) return 'MyObj:' .. t.name end

-- __eq / __lt / __le:比较重载
-- __add / __sub / __mul:算术重载

local t = setmetatable({}, mt)

TS 对照

Lua TS 近似
__index Proxy.get / 原型链
__newindex Proxy.set
__call 把对象当函数调(TS 需要 interface { (args): ret } 表达)
__tostring toString()

手写类

local Animal = {}
Animal.__index = Animal           -- 让实例找不到字段时回 Animal

function Animal.new(name)
  local self = setmetatable({}, Animal)
  self.name = name
  return self
end

function Animal:speak() print(self.name, 'speaks') end

-- 继承
local Dog = setmetatable({}, { __index = Animal })
Dog.__index = Dog

function Dog.new(name)
  local self = Animal.new(name)         -- super()
  return setmetatable(self, Dog)
end

function Dog:speak() print(self.name, 'barks') end

local d = Dog.new('Rex')
d:speak()                                 -- Rex barks

9. 模块 / require

-- 一个模块 = 一个返回 table 的文件
-- lua/myplug/init.lua
local M = {}
M.foo = function() return 1 end
function M.bar() return 2 end
return M

-- 调用方
local myplug = require('myplug')          -- lua/myplug/init.lua
local util = require('myplug.util')       -- lua/myplug/util.lua

规则

  • . 当目录分隔符
  • 自动加 lua/ 前缀和 .lua 后缀
  • require 会缓存:第二次调用返回同一个 table
  • 热更:package.loaded['myplug'] = nil; require('myplug')(常见调试技巧)

10. 错误处理

-- 抛错
error('oops')                     -- 带栈信息
error({ code = 1, msg = 'oops' }) -- 也能抛任意对象

-- 捕获(TS 的 try/catch)
local ok, err = pcall(function()
  error('boom')
end)
if not ok then print('caught:', err) end

-- 带上下文
local ok, err = pcall(dangerous_fn, arg1, arg2)

-- xpcall:加自定义 handler(如 traceback)
local ok, err = xpcall(fn, debug.traceback)

-- 主动 assert(失败抛错)
assert(x > 0, 'x must be positive')

约定:失败 return nil, err_msg(Go 风格),而不是 throw

local function parse(s)
  if s == '' then return nil, 'empty input' end
  return tonumber(s)
end
local n, err = parse('')
if not n then return nil, err end

11. 协程(coroutine)

类比 TS 的 generator + async 混合

local co = coroutine.create(function(x)
  coroutine.yield(x + 1)
  coroutine.yield(x + 2)
  return x + 3
end)

local ok, v = coroutine.resume(co, 10)   -- ok=true, v=11
ok, v = coroutine.resume(co)             -- v=12
ok, v = coroutine.resume(co)             -- v=13(return)
coroutine.status(co)                     -- 'dead'

Neovim 里少用(因为有 vim.schedule 和 callback-style);plenary.async 基于这个封装


12. 标准库对照 TS

string(所有是 byte 级别,非 UTF-8)

Lua TS 对照
#s s.length(字节数,非字符数)
s:sub(i, j) s.slice(i-1, j)
s:upper() / s:lower() s.toUpperCase()
s:rep(n) s.repeat(n)
s:find(pat) s.search(re) + match
s:match(pat) s.match(re)
s:gmatch(pat) 迭代器版
s:gsub(pat, repl) s.replace(re, repl)
s:byte(i) s.charCodeAt(i-1)
s:format(...) / string.format printf 风 / 模板字符串
table.concat({'a','b'}, ',') ['a','b'].join(',')
无内置 split vim.split(s, sep)

Lua pattern 很弱(不是正则)。要正则用 vim.regex(pat)vim.re(LPeg)

-- Lua pattern 常用
'%a'  -- 字母  ≈ [a-zA-Z]
'%d'  -- 数字  ≈ [0-9]
'%s'  -- 空白  ≈ \s
'%w'  -- 字母数字
'%p'  -- 标点
'.'   -- 任意
'*'   -- 贪婪 0+
'+'   -- 贪婪 1+
'-'   -- 非贪婪 0+(注意不是 ?)
'^'   -- 字符串开头
'$'   -- 字符串结尾
'%x'  -- 转义特殊字符:%. %% %( %)

table

Lua TS 对照
table.insert(t, v) arr.push(v)
table.insert(t, 1, v) arr.unshift(v)
table.remove(t) arr.pop()
table.remove(t, 1) arr.shift()
table.sort(t, cmp) arr.sort(cmp)
table.concat(t, sep) arr.join(sep)
table.unpack(t) / unpack(t) ...arr(spread)
#t arr.length(不可靠,见 §7)

Neovim 扩展:vim.iter(t):filter():map():totable() 更接近 TS 的链式

math

math.pi             math.huge            math.maxinteger   math.mininteger
math.floor(x)       math.ceil(x)         math.abs(x)       math.sign -- 无,自己写
math.min(a, b, ...) math.max(a, b, ...)
math.sqrt(x)        math.pow(x, y) -- 5.1 有;5.3+ 用 x^y
math.random()       math.random(n)       math.random(m, n)
math.randomseed(s)
math.log(x)         math.log(x, base)    math.exp(x)
math.sin / cos / tan / asin / acos / atan
math.fmod(x, y)     math.modf(x)  -- 返回整数部分, 小数部分
math.tointeger(x)   math.type(x)  -- 'integer' / 'float' / nil

io / os

-- 文件 IO
local f = io.open('/tmp/x', 'r')          -- Node 的 fs.openSync
local content = f:read('*a')              -- 全读
f:close()

-- 写入
io.open('/tmp/x', 'w'):write('hi'):close()

-- 一次性读取所有行
for line in io.lines('/tmp/x') do print(line) end

-- OS
os.time()                                 -- epoch 秒
os.date('%Y-%m-%d')                       -- strftime 风
os.clock()                                -- CPU 时间
os.getenv('HOME')
os.execute('ls')                          -- 阻塞,尽量用 vim.system
os.exit(0)
os.tmpname()                              -- 临时文件名

Neovim 里:别用 io.*os.execute 做 IO。用 vim.uvvim.system


13. TS 开发者的十大坑

  1. 忘写 local → 变成全局变量。开 lua-language-server 会警告
  2. 1-based 索引s:sub(1, 3) 是前 3 个字符,不是 s.slice(1, 3)
  3. 0'' 是 truthyif x then 不能代替 if x != null
  4. #t 不可靠 → 遇 nil 就断。数组含空洞时自己维护长度
  5. pairs 顺序不保证 → 需要顺序用 ipairs
  6. nil 赋给 table 字段 = 删除t.x = nil 不是"置空",是"删 x"
  7. 无 continue → 用 goto continue + ::continue::
  8. 多返回值被吃掉f() 在中间位置只取第一个;最后一个位置才展开
  9. and/or 是值运算flag or true 在 flag=false 时变 true
  10. 字符串是 byte → 处理中文用 vim.fn.strdisplaywidth / vim.str_utfindex / vim.fn.strcharlen

14. 典型 TS → Lua 翻译

异步处理文件

// TS
async function main() {
  try {
    const content = await fs.readFile('/tmp/x', 'utf8')
    const lines = content.split('\n').filter(l => l.length > 0)
    console.log(lines)
  } catch (e) {
    console.error(e)
  }
}
-- Lua (用 vim.uv 回调)
vim.uv.fs_open('/tmp/x', 'r', 438, function(err, fd)
  if err then return vim.schedule(function() vim.notify(err, vim.log.levels.ERROR) end) end
  vim.uv.fs_fstat(fd, function(_, stat)
    vim.uv.fs_read(fd, stat.size, 0, function(_, data)
      vim.uv.fs_close(fd)
      vim.schedule(function()
        local lines = vim.split(data, '\n', { plain = true })
        lines = vim.tbl_filter(function(l) return #l > 0 end, lines)
        print(vim.inspect(lines))
      end)
    end)
  end)
end)

-- 或者同步(Neovim 里够用)
local content = table.concat(vim.fn.readfile('/tmp/x'), '\n')
local lines = vim.split(content, '\n', { plain = true })
lines = vim.tbl_filter(function(l) return #l > 0 end, lines)

class + 继承

// TS
class Shape {
  constructor(public name: string) {}
  area() { return 0 }
  describe() { return `${this.name}: ${this.area()}` }
}
class Circle extends Shape {
  constructor(public r: number) { super('circle') }
  area() { return Math.PI * this.r ** 2 }
}
-- Lua
local Shape = {}
Shape.__index = Shape

function Shape.new(name)
  return setmetatable({ name = name }, Shape)
end
function Shape:area() return 0 end
function Shape:describe()
  return ('%s: %s'):format(self.name, self:area())
end

local Circle = setmetatable({}, { __index = Shape })
Circle.__index = Circle

function Circle.new(r)
  local self = Shape.new('circle')
  self.r = r
  return setmetatable(self, Circle)
end
function Circle:area() return math.pi * self.r ^ 2 end

local c = Circle.new(5)
print(c:describe())   -- circle: 78.539816339745

15. 推荐阅读

Clone this wiki locally