简单了解 with

155 阅读5分钟

with 语句详解:使用方法和 this 指向问题

目录

  1. with 语句基础
  2. 作用域链查找机制
  3. this 指向问题(核心)
  4. window 对象的指向
  5. 在微前端沙箱中的应用
  6. 实际代码示例
  7. 常见问题和注意事项
  8. 最佳实践

with 语句基础

什么是 with 语句?

with 语句是 JavaScript 的一个特性,它可以将一个对象添加到作用域链的最前端,使得在 with 块内可以直接访问该对象的属性,而不需要使用对象名作为前缀。

基本语法


with (object) {
  // 在这个块内,可以直接访问 object 的属性
  // 不需要写 object.property,直接写 property 即可
}

基本示例

const obj = {
  name: "Alice",
  age: 25,
  city: "Beijing",
};

// 使用 with 语句
with (obj) {
  // 可以直接访问 obj 的属性,不需要 obj. 前缀
  console.log(name); // 输出: Alice
  console.log(age); // 输出: 25
  console.log(city); // 输出: Beijing

  // 也可以修改属性
  age = 26;
  console.log(age); // 输出: 26
}

// with 块外,需要正常访问
console.log(obj.age); // 输出: 26

作用域链查找机制

查找顺序

with 块内,当访问一个变量时,JavaScript 引擎会按照以下顺序查找:

  1. 局部变量let/const/var 声明的)
  2. with 指定的对象with(obj) 中的 obj
  3. 外层作用域
  4. 全局作用域

示例:作用域链查找

const globalVar = "全局变量";

function testScope() {
  const localVar = "局部变量";

  const obj = {
    objVar: "对象变量",
    localVar: "对象中的 localVar",
  };

  with (obj) {
    // 1. 查找局部变量 localVar(在函数作用域中)
    // 2. 如果没找到,查找 obj 中的属性
    // 3. 如果还没找到,查找外层作用域
    // 4. 最后查找全局作用域

    console.log(localVar); // 输出: "局部变量"(优先使用函数作用域的)
    console.log(objVar); // 输出: "对象变量"(在 obj 中找到)
    console.log(globalVar); // 输出: "全局变量"(在外层作用域找到)
  }
}

testScope();

this 指向问题(核心)

关键点:this 不受 with 语句影响

这是最重要的知识点! this 的指向完全不受 with 语句的影响。

this 的指向规则

this 的指向遵循 JavaScript 的标准规则:

  • this 指向函数调用时的上下文(caller)
  • 在全局作用域中,this 指向全局对象(浏览器中是 window
  • 在对象方法中,this 指向调用该方法的对象
  • with 语句不会改变 this 的指向

示例 1:this 在 with 块内的行为

const obj = {
  name: "Context Object",
  test: function () {
    console.log("this.name:", this.name);
    return this;
  },
};

const sandboxContext = {
  window: { name: "Sandbox Window" },
  obj: obj,
};

// 使用 Function 构造器创建函数(避免严格模式限制)
const func = new Function(
  "sandboxContext",
  `
  with(sandboxContext) {
    // window 指向 sandboxContext.window
    console.log("window.name:", window.name); // 输出: Sandbox Window
    
    // 但是 this 仍然指向全局对象,不受 with 影响
    // 注意:这里的 window 是 sandboxContext.window(代理对象),不是全局 window
    console.log("this === window:", this === window); // 输出: false(this 是全局 window,window 是代理对象)
    console.log("this === sandboxContext.window:", this === sandboxContext.window); // 输出: false
    
    // 调用 obj.test(),this 指向 obj,不是 sandboxContext
    const result = obj.test(); // 输出: this.name: Context Object
    console.log("obj.test() 返回的 this === obj:", result === obj); // 输出: true
  }
  `
);

func(sandboxContext);

示例 2:对比 window 和 this

const proxyWindow = new Proxy(window, {
  get(target, prop) {
    console.log(`[Proxy] 读取属性: ${String(prop)}`);
    return target[prop];
  },
  set(target, prop, value) {
    console.log(`[Proxy] 设置属性: ${String(prop)} = ${value}`);
    target[prop] = value;
    return true;
  },
});

const sandboxContext = {
  window: proxyWindow,
  document: proxyWindow.document,
  console: proxyWindow.console,
};

const code = `
  // 在 with 块内,window 指向 sandboxContext.window(即 proxyWindow)
  window.myVar = "hello from sandbox";
  console.log("window.myVar:", window.myVar);
  
  // this 仍然指向全局 window(不受 with 影响)
  // 注意:这里的 window 是 sandboxContext.window(代理对象),不是全局 window
  console.log("this === window:", this === window); // 输出: false(this 是全局 window,window 是代理对象)
  console.log("this === sandboxContext.window:", this === sandboxContext.window); // 输出: false
  
  // 但是代码中的 window 指向 proxyWindow
  console.log("代码中的 window === proxyWindow:", window === sandboxContext.window); // 输出: true
`;

const func = new Function(
  "sandboxContext",
  `
  with(sandboxContext) {
    ${code}
  }
  `
);

func(sandboxContext);

为什么 this 不受影响?

this 是 JavaScript 的一个特殊关键字,它的值在函数调用时确定,与作用域链无关。with 语句只影响作用域链的查找,不会影响 this 的绑定。

重要区别:window 和 this 在 with 块内

with(sandboxContext) 块内,需要理解以下关键区别:

  1. window 的指向

    • window 会指向 sandboxContext.window(代理对象)
    • 这是通过作用域链查找实现的
  2. this 的指向

    • this 仍然指向全局对象(不受 with 影响)
    • 这是 JavaScript 的 this 绑定机制决定的
  3. 因此

    with (sandboxContext) {
      // window 是 sandboxContext.window(代理对象)
      // this 是全局 window
      console.log(this === window); // false(它们不相等!)
    }
    

关键理解

  • window 标识符通过作用域链查找,在 sandboxContext 中找到,所以指向代理对象
  • this 关键字不受作用域链影响,仍然指向全局对象
  • 这就是为什么在微前端沙箱中,代码里的 window 可以指向代理对象,但 this 仍然指向全局对象

window 对象的指向

关键机制

在微前端沙箱实现中,with 语句的核心作用是替换 window 对象的引用

为什么不能直接用 with(proxyWindow)

// ❌ 错误的方式
const proxyWindow = new Proxy(window, {
  /* ... */
});
with (proxyWindow) {
  window.myVar = "hello"; // window 会去外层作用域查找,找到全局 window
}

问题:如果直接使用 with(proxyWindow),代码中的 window 标识符会去外层作用域查找,找到全局的 window 对象,而不是 proxyWindow

正确的做法

// ✅ 正确的方式
const proxyWindow = new Proxy(window, {
  /* ... */
});

// 创建一个包含 window 属性的上下文对象
const sandboxContext = {
  window: proxyWindow, // 关键:将 proxyWindow 作为 window 属性
  document: proxyWindow.document,
  console: proxyWindow.console,
};

with (sandboxContext) {
  // 现在 window 会在 sandboxContext 中查找
  // 找到 sandboxContext.window(即 proxyWindow)
  window.myVar = "hello"; // 实际访问的是 proxyWindow.myVar
}

执行流程

子应用代码: window.myVar = 'hello'
       ↓
sandboxContext = { window: proxyWindow }
with(sandboxContext) { window.myVar = 'hello' }
       ↓
代码中的 window 在 sandboxContext 中查找
找到 sandboxContext.window(即代理对象 proxyWindow)
       ↓
访问 proxyWindow.myVar,触发 Proxy 的 set 拦截器
       ↓
值被写入 fakeWindow.myVar,而不是真实的 window

在微前端沙箱中的应用

完整的沙箱实现

class WindowProxySandbox {
  private proxy: Window;
  private fakeWindow: Record<string, any> = {};
  private updatedValueSet = new Set<string>();

  constructor() {
    this.fakeWindow = Object.create(null);

    this.proxy = new Proxy(window, {
      get: (_target: Window, prop: string) => {
        // 如果属性在 fakeWindow 中存在,优先返回 fakeWindow 的值
        if (this.updatedValueSet.has(prop)) {
          return this.fakeWindow[prop];
        }
        // 否则返回原始 window 的值
        return (window as any)[prop];
      },

      set: (_target: Window, prop: string, value: any) => {
        // 所有修改都记录到 fakeWindow 中
        this.fakeWindow[prop] = value;
        this.updatedValueSet.add(prop);
        return true;
      },

      has: (_target: Window, prop: string) => {
        return prop in this.fakeWindow || prop in window;
      },

      deleteProperty: (_target: Window, prop: string) => {
        if (this.updatedValueSet.has(prop)) {
          delete this.fakeWindow[prop];
          this.updatedValueSet.delete(prop);
        }
        return true;
      },

      ownKeys: (_target: Window) => {
        const originalKeys = Reflect.ownKeys(window);
        const fakeKeys = Reflect.ownKeys(this.fakeWindow);
        return Array.from(new Set([...originalKeys, ...fakeKeys]));
      },
    });
  }

  /**
   * 执行子应用代码(推荐方式)
   */
  execScriptWith(script: string): any {
    // 创建沙箱上下文,将代理 window 作为属性
    const sandboxContext = {
      window: this.proxy,
      document: (this.proxy as any).document,
      location: (this.proxy as any).location,
      console: (this.proxy as any).console,
    };

    // 使用 Function 构造器创建函数
    const func = new Function(
      "sandboxContext",
      `
      with(sandboxContext) {
        ${script}
      }
    `
    );

    // 执行函数,传入 sandboxContext
    return func(sandboxContext);
  }

  getProxy(): Window {
    return this.proxy;
  }
}

使用示例

const sandbox = new WindowProxySandbox();

// 子应用的代码(模拟从远程加载的代码)
const appCode = `
  // 子应用代码中直接使用 window,不需要任何修改
  window.myApp = 'sub-app-proxy';
  window.myConfig = { version: '1.0.0', env: 'production' };
  
  // 访问 window 的其他属性也会被代理
  console.log('window.location:', window.location);
  
  // 设置全局变量
  window.globalVar = 'hello from sub app';
`;

// 执行子应用代码
sandbox.execScriptWith(appCode);

// 验证隔离性
console.log("代理 window.myApp:", sandbox.getProxy().myApp); // 输出: sub-app-proxy
console.log("真实 window.myApp:", window.myApp); // 输出: undefined(未被污染)

实际代码示例

示例 1:基本 with 使用

function example1_BasicWith() {
  const obj = {
    name: "Alice",
    age: 25,
    city: "Beijing",
  };

  // 使用 Function 构造器(避免严格模式限制)
  const func = new Function(
    "obj",
    `
    with(obj) {
      // 在 with 块内,可以直接访问 obj 的属性
      console.log("name:", name);        // 输出: Alice
      console.log("age:", age);          // 输出: 25
      console.log("city:", city);        // 输出: Beijing
      
      // 也可以修改属性
      age = 26;
      console.log("修改后的 age:", age); // 输出: 26
    }
    
    // with 块外,需要正常访问
    console.log("with 块外访问:", obj.age); // 输出: 26
  `
  );

  func(obj);
}

示例 2:this 指向演示

function example2_ThisBinding() {
  const obj = {
    name: "Context Object",
    test: function () {
      console.log("this.name:", this.name);
      return this;
    },
  };

  const sandboxContext = {
    window: { name: "Sandbox Window" },
    obj: obj,
  };

  const func = new Function(
    "sandboxContext",
    `
    with(sandboxContext) {
      // window 指向 sandboxContext.window
      console.log("window.name:", window.name); // 输出: Sandbox Window
      
      // 但是 this 仍然指向全局对象,不受 with 影响
      // 注意:这里的 window 是 sandboxContext.window(代理对象),不是全局 window
      console.log("this === window:", this === window); // 输出: false(this 是全局 window,window 是代理对象)
      console.log("this === sandboxContext.window:", this === sandboxContext.window); // 输出: false
      
      // 调用 obj.test(),this 指向 obj,不是 sandboxContext
      const result = obj.test(); // 输出: this.name: Context Object
      console.log("obj.test() 返回的 this === obj:", result === obj); // 输出: true
    }
  `
  );

  func(sandboxContext);
}

示例 3:微前端沙箱应用

function example3_MicroFrontendSandbox() {
  // 创建代理 window
  const proxyWindow = new Proxy(window, {
    get(target, prop) {
      console.log(`[Proxy] 读取属性: ${String(prop)}`);
      return target[prop];
    },
    set(target, prop, value) {
      console.log(`[Proxy] 设置属性: ${String(prop)} = ${value}`);
      target[prop] = value;
      return true;
    },
  });

  const sandboxContext = {
    window: proxyWindow,
    document: proxyWindow.document,
    console: proxyWindow.console,
  };

  const code = `
    // 在 with 块内,window 指向 sandboxContext.window(即 proxyWindow)
    window.myVar = "hello from sandbox";
    console.log("window.myVar:", window.myVar);
    
    // this 仍然指向全局 window(不受 with 影响)
    // 注意:这里的 window 是 sandboxContext.window(代理对象),不是全局 window
    console.log("this === window:", this === window); // 输出: false(this 是全局 window,window 是代理对象)
    console.log("this === sandboxContext.window:", this === sandboxContext.window); // 输出: false
    
    // 但是代码中的 window 指向 proxyWindow
    console.log("代码中的 window === proxyWindow:", window === sandboxContext.window); // 输出: true
  `;

  const func = new Function(
    "sandboxContext",
    `
    with(sandboxContext) {
      ${code}
    }
  `
  );

  func(sandboxContext);

  console.log("\n验证结果:");
  console.log("proxyWindow.myVar:", proxyWindow.myVar);
  console.log("真实 window.myVar:", window.myVar);
}

示例 4:with 内外对比

function example4_CompareInsideAndOutside() {
  const sandboxContext = {
    window: { name: "Proxy Window", myVar: "sandbox value" },
    globalVar: "I'm in sandbox",
  };

  const func = new Function(
    "sandboxContext",
    `
    // with 块外
    console.log("=== with 块外 ===");
    console.log("window:", typeof window); // 输出: object(全局 window)
    console.log("globalVar:", typeof globalVar); // 输出: undefined(未定义)
    
    // with 块内
    with(sandboxContext) {
      console.log("=== with 块内 ===");
      // window 现在指向 sandboxContext.window
      console.log("window.name:", window.name); // 输出: Proxy Window
      console.log("window.myVar:", window.myVar); // 输出: sandbox value
      
      // globalVar 现在指向 sandboxContext.globalVar
      console.log("globalVar:", globalVar); // 输出: I'm in sandbox
      
      // this 仍然指向全局对象(不受 with 影响)
      // 注意:这里的 window 是 sandboxContext.window(代理对象),不是全局 window
      console.log("this === window:", this === window); // 输出: false(this 是全局 window,window 是代理对象)
      console.log("this === sandboxContext.window:", this === sandboxContext.window); // 输出: false
    }
    
    // with 块外,恢复原状
    console.log("=== with 块外(恢复)===");
    console.log("window:", typeof window); // 输出: object(全局 window)
    console.log("globalVar:", typeof globalVar); // 输出: undefined
  `
  );

  func(sandboxContext);
}

常见问题和注意事项

1. 严格模式限制

问题with 语句在严格模式下不能直接使用。

"use strict";
with (obj) {
  // ❌ SyntaxError: Strict mode code may not include a with statement
  // ...
}

解决方案:使用 Function 构造器动态创建函数(不在严格模式下执行)。

// ✅ 正确的方式
const func = new Function(
  "sandboxContext",
  `
  with(sandboxContext) {
    // 代码在这里执行
  }
  `
);

2. 性能考虑

with 语句会影响 JavaScript 引擎的优化,因为引擎无法在编译时确定变量的作用域。但在微前端沙箱场景中,这是必要的权衡。

3. 调试困难

使用 with 语句会让代码调试变得困难,因为变量的实际来源不明确。建议:

  • 添加详细的日志
  • 使用清晰的变量命名
  • 在开发环境中禁用 with,使用其他方式

4. 作用域污染

with 语句会将对象的所有属性添加到作用域链中,可能导致意外的变量覆盖。

const obj = {
  console: "这不是 console 对象",
  log: "这也不是 log 方法",
};

with (obj) {
  console.log("这可能会出错!"); // ❌ 因为 console 被覆盖了
}

解决方案:只将必要的属性添加到 sandboxContext 中。


最佳实践

1. 始终使用 sandboxContext 对象

// ❌ 错误:不要直接用 with(proxyWindow)
with (proxyWindow) {
  window.myVar = "hello"; // window 会去外层作用域查找
}

// ✅ 正确:使用 sandboxContext 对象
const sandboxContext = {
  window: proxyWindow,
  document: proxyWindow.document,
  console: proxyWindow.console,
};

with (sandboxContext) {
  window.myVar = "hello"; // window 指向 sandboxContext.window
}

2. 处理全局对象

除了 window,还要处理其他全局对象:

const sandboxContext = {
  window: proxyWindow,
  document: proxyWindow.document,
  location: proxyWindow.location,
  console: proxyWindow.console,
  // 根据需要添加其他全局对象
};

3. 错误处理

子应用代码执行可能出错,需要添加错误处理:

try {
  sandbox.execScriptWith(appCode);
} catch (error) {
  console.error("执行子应用代码时出错:", error);
  // 记录错误日志,便于调试
}

4. 性能优化

  • 避免频繁创建和销毁沙箱
  • 缓存常用的全局对象引用
  • 只在必要时使用 with 语句

5. 兼容性考虑

  • 检查浏览器是否支持 Proxy(现代浏览器都支持)
  • 不支持时降级到快照沙箱(SnapshotSandbox)

6. 安全性

  • 只加载可信的子应用代码
  • 限制子应用访问某些敏感 API
  • 使用 CSP(Content Security Policy)限制代码执行

总结

核心要点

  1. with 语句的作用

    • 将对象添加到作用域链的最前端
    • with 块内可以直接访问对象的属性
  2. this 的指向(最重要)

    • this 不受 with 语句影响
    • this 始终指向函数调用时的上下文
    • with 块内,this 仍然指向原来的对象(通常是全局对象)
  3. window 的指向

    • 不能直接用 with(proxyWindow)
    • 应该用 sandboxContext = { window: proxyWindow } 然后 with(sandboxContext)
    • 这样代码中的 window 会指向 sandboxContext.window(即代理对象)
  4. 作用域链查找顺序

    • 局部变量 → with 指定的对象 → 外层作用域 → 全局作用域
  5. 实际应用

    • 微前端沙箱实现的核心机制
    • 通过 with 语句替换 window 引用
    • 结合 Proxy 实现完全隔离

关键代码模式

// 1. 创建代理 window
const proxyWindow = new Proxy(window, {
  /* ... */
});

// 2. 创建沙箱上下文
const sandboxContext = {
  window: proxyWindow,
  document: proxyWindow.document,
  // ... 其他全局对象
};

// 3. 使用 with 语句执行代码
const func = new Function(
  "sandboxContext",
  `
  with(sandboxContext) {
    ${appCode}
  }
  `
);

func(sandboxContext);

参考资料