如何在JavaScript中模拟指针?

How to simulate pointers in JavaScript?

我正在创建一种可编译为 Swift、Rust 和 JavaScript(或至少尝试)的语言。 Rust 和 Swift 都使用 pointers/references/dereferencing/etc.,而 JavaScript 不使用。所以在类似 Rust 的语言中,你可能会这样做:

fn update(x) {
  *x++
}

fn main() {
  let i = 0
  update(&i)
  log(i) #=> 1
}

在类似 JavaScript 的语言中,如果您这样做,它将失败:

function update(x) {
  x++
}

function main() {
  let i = 0
  update(i)
  log(i) #=> 0
}

因为值在传入时被克隆(我们显然知道)。

所以我首先考虑的是这样做:

function update(scopeWithI) {
  scopeWithI.i++
}

function main() {
  let i = 0
  let scopeWithI = { i }
  update(scopeWithI)
  i = scopeWithI.i
  log(i) #=> 1
}

但这需要进行大量额外处理,而且看起来没有必要。相反,我可能会尝试编译为:

function update(scopeWithI) {
  scopeWithI.i++
}

function main() {
  let scope = {}
  scope.i = 0
  update(scope)
  log(scope.i) #=> 1
}

这意味着您创建的每个嵌套范围,您都必须手动启动 creating/managing 范围链。实际上那是行不通的,因为 update 被硬编码为 i。所以你可能必须传入你想要的变量名。

function update(scope, ...names) {
  scope[names[0]]++
}

但后来就像:

function update(scope, ...names) {
  scope[names[0]]++
}

function main() {
  let scope = {}
  scope.i = 0
  if random() > 0.5
    let childScope = { scope }
    childScope.x = 0
    update(childScope, ['i'])
    update(childScope, ['x'])
    update(childScope, ['x'])
    log(childScope.x) #=> 2
  else
    update(childScope, ['i'])

  log(scope.i) #=> 1
}

所以这似乎可以让我们到达某个地方

所以一般的解决方案是将范围作为函数的第一个参数。

function add(scope, name1, name2) {
  return scope[name1] + scope[name2]
}

取消引用意味着直接从作用域读取值,而传递引用(如 Rust 或 C 中的 &name)意味着传递作用域和名称。

这样的东西行得通吗?或者更确切地说,需要更改或添加什么?需要比这更复杂吗?

我想尝试找到一种方法将面向指针的代码转换为JavaScript(transpilation),而不是首先尝试找出不那么直接的看似复杂得多的方法,并通过重新定义许多方法来避免 JavaScript 中的指针模拟。似乎避免在 JavaScript 中使用任何指针会更难弄清楚,所以我试图看看是否可以在 JavaScript.

中模拟指针类型的系统

要避免指针模拟,您将不得不重新定义方法。

update(x) {
  *x++
}

必须在所有地方更改函数的外部用法。所以这个:

main() {
  let i = 0
  update(&i)
}

会变成:

main() {
  let i = 0
  i++ // inline the thing
}

对于这种简单的情况,它很好,但对于更复杂的函数,它开始看起来像宏,并且可能会变得复杂。

所以我们没有改变外部用法,而是让你必须通过范围。

另一种方法可能是让每个变量都是一个有值的对象,所以它更像是:

update(x) {
  x.value++
}

main() {
  let i = { value: 0 }
  update(i)
}

那我就在想,那怎么处理对引用的引用呢?

update2(x) {
  update(&x)
}

update(x) {
  *x++
}

main() {
  let i = 0
  update2(&i)
}

在我描述的系统中,就像:

update2(x) {
  // then what?
  let y = { value: x }
  update(y)
}

update(x) {
  // like this?
  x.value.value++
}

main() {
  let i = { value: 0 }
  update2(i)
}

所以这似乎真的行不通。

JavaScript 对象“通过引用”传递:

const obj = {
  hello: "world"
};

console.log(obj.hello); // "world"
f(obj);
console.log(obj.hello); // "bob"

function f(s) {
  s.hello = "bob";
}

因此,如果您想要像 C 或 Rust 或 Swift 这样的“引用”,请将所有内容都放在对象中。考虑

fn update(x) {
  x++
}

fn main() {
  let i = 0
  update(&i)
  log(i) #=> 1
}

在 JavaScript 中将是:

function update(obj) {
  obj.x++
}

const i = { x: 0 };
update(i);
console.log(i.x); // 1

这个有用还是我误会了?

update2(x) {
  update(x)
}

update(x) {
  x.value++
}

main() {
  let i = { value: 0 }
  update2(i)
}

不用说,但是 JavaScript 没有 有一个通过引用传递参数的通用机制。

“引用”一词可能会引起一些混淆,因为 JavaScript 可以将对象传递给函数——引用——但这是call-by-value 机制。 Call-by-reference 实际上意味着参数变量是调用者变量的 别名 ,这样 分配 到该别名等同于分配给调用者的变量。除了一些非常特殊的情况(比如 arguments exotic object in non-strict 模式,或者 export 机制,或者 var link with window对象,none 其中以最佳实践方式帮助你),JavaScript 中没有这样的 variable-aliasing 机制。

这是一个关于如何在 non-strict 模式下在浏览器上下文中“利用”var 效果的示例:

function modify(ref) {
    // Using the fact that global `var` variables are aliases
    //  for properties on the global object
    // (This is not considered good practice)
    globalThis[ref] = globalThis[ref] + 1;
}

var a = 1;
modify("a"); // We pass a reference to `a`
console.log(a);

简而言之,除了旧 JavaScript 语言中的一些糟糕设计(在草率模式下)外,在 JavaScript 中通常是不可能的。您所有成功的尝试,通过设置其属性之一,对给定对象执行 突变 。您不能希望有一个函数将 赋值 给参数变量,从而修改调用者的变量。这是不可能的——设计使然。请注意 assignmentmutation.

之间的重要区别

如果调用者的变量(不是属性)需要分配一个新值(不只是突变,而是真正的赋值),那么那个赋值 必须 发生在 那个 变量上——函数不能为调用者做的事情。

因此,在 JavaScript 中执行此类赋值的方法是,将函数 return 设为调用者需要重新赋值的任何内容,并且它仍然由调用者负责执行该分配:

function modify(value) {
    return 3;
}

let value = 1;
value = modify(value);
console.log(value);

当涉及多个变量时,让函数 return 成为一个“打包”对象,调用者可以将其解构为自己的变量:

function modify(a, b) {
    return [a + 1, b * 2];
}

let a = 1, b = 2;
[a, b] = modify(a, b);
console.log(a, b);