with 语句详解:使用方法和 this 指向问题
目录
- [with 语句基础](#with 语句基础 "#with-%E8%AF%AD%E5%8F%A5%E5%9F%BA%E7%A1%80")
- 作用域链查找机制
- [this 指向问题(核心)](#this 指向问题(核心) "#this-%E6%8C%87%E5%90%91%E9%97%AE%E9%A2%98%E6%A0%B8%E5%BF%83")
- [window 对象的指向](#window 对象的指向 "#window-%E5%AF%B9%E8%B1%A1%E7%9A%84%E6%8C%87%E5%90%91")
- 在微前端沙箱中的应用
- 实际代码示例
- 常见问题和注意事项
- 最佳实践
with 语句基础
什么是 with 语句?
with 语句是 JavaScript 的一个特性,它可以将一个对象添加到作用域链的最前端,使得在 with 块内可以直接访问该对象的属性,而不需要使用对象名作为前缀。
基本语法
javascript
with (object) {
// 在这个块内,可以直接访问 object 的属性
// 不需要写 object.property,直接写 property 即可
}
基本示例
ts
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 引擎会按照以下顺序查找:
- 局部变量 (
let/const/var声明的) - with 指定的对象 (
with(obj)中的obj) - 外层作用域
- 全局作用域
示例:作用域链查找
javascript
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 块内的行为
javascript
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
javascript
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) 块内,需要理解以下关键区别:
-
window的指向:window会指向sandboxContext.window(代理对象)- 这是通过作用域链查找实现的
-
this的指向:this仍然指向全局对象(不受with影响)- 这是 JavaScript 的
this绑定机制决定的
-
因此:
javascriptwith (sandboxContext) { // window 是 sandboxContext.window(代理对象) // this 是全局 window console.log(this === window); // false(它们不相等!) }
关键理解:
window标识符通过作用域链查找,在sandboxContext中找到,所以指向代理对象this关键字不受作用域链影响,仍然指向全局对象- 这就是为什么在微前端沙箱中,代码里的
window可以指向代理对象,但this仍然指向全局对象
window 对象的指向
关键机制
在微前端沙箱实现中,with 语句的核心作用是替换 window 对象的引用。
为什么不能直接用 with(proxyWindow)?
javascript
// ❌ 错误的方式
const proxyWindow = new Proxy(window, {
/* ... */
});
with (proxyWindow) {
window.myVar = "hello"; // window 会去外层作用域查找,找到全局 window
}
问题 :如果直接使用 with(proxyWindow),代码中的 window 标识符会去外层作用域查找,找到全局的 window 对象,而不是 proxyWindow。
正确的做法
javascript
// ✅ 正确的方式
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
}
执行流程
ts
子应用代码: window.myVar = 'hello'
↓
sandboxContext = { window: proxyWindow }
with(sandboxContext) { window.myVar = 'hello' }
↓
代码中的 window 在 sandboxContext 中查找
找到 sandboxContext.window(即代理对象 proxyWindow)
↓
访问 proxyWindow.myVar,触发 Proxy 的 set 拦截器
↓
值被写入 fakeWindow.myVar,而不是真实的 window
在微前端沙箱中的应用
完整的沙箱实现
javascript
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;
}
}
使用示例
javascript
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 使用
javascript
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 指向演示
javascript
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:微前端沙箱应用
javascript
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 内外对比
javascript
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 语句在严格模式下不能直接使用。
javascript
"use strict";
with (obj) {
// ❌ SyntaxError: Strict mode code may not include a with statement
// ...
}
解决方案 :使用 Function 构造器动态创建函数(不在严格模式下执行)。
javascript
// ✅ 正确的方式
const func = new Function(
"sandboxContext",
`
with(sandboxContext) {
// 代码在这里执行
}
`
);
2. 性能考虑
with 语句会影响 JavaScript 引擎的优化,因为引擎无法在编译时确定变量的作用域。但在微前端沙箱场景中,这是必要的权衡。
3. 调试困难
使用 with 语句会让代码调试变得困难,因为变量的实际来源不明确。建议:
- 添加详细的日志
- 使用清晰的变量命名
- 在开发环境中禁用
with,使用其他方式
4. 作用域污染
with 语句会将对象的所有属性添加到作用域链中,可能导致意外的变量覆盖。
javascript
const obj = {
console: "这不是 console 对象",
log: "这也不是 log 方法",
};
with (obj) {
console.log("这可能会出错!"); // ❌ 因为 console 被覆盖了
}
解决方案 :只将必要的属性添加到 sandboxContext 中。
最佳实践
1. 始终使用 sandboxContext 对象
javascript
// ❌ 错误:不要直接用 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,还要处理其他全局对象:
javascript
const sandboxContext = {
window: proxyWindow,
document: proxyWindow.document,
location: proxyWindow.location,
console: proxyWindow.console,
// 根据需要添加其他全局对象
};
3. 错误处理
子应用代码执行可能出错,需要添加错误处理:
javascript
try {
sandbox.execScriptWith(appCode);
} catch (error) {
console.error("执行子应用代码时出错:", error);
// 记录错误日志,便于调试
}
4. 性能优化
- 避免频繁创建和销毁沙箱
- 缓存常用的全局对象引用
- 只在必要时使用
with语句
5. 兼容性考虑
- 检查浏览器是否支持
Proxy(现代浏览器都支持) - 不支持时降级到快照沙箱(SnapshotSandbox)
6. 安全性
- 只加载可信的子应用代码
- 限制子应用访问某些敏感 API
- 使用 CSP(Content Security Policy)限制代码执行
总结
核心要点
-
with 语句的作用:
- 将对象添加到作用域链的最前端
- 在
with块内可以直接访问对象的属性
-
this 的指向(最重要):
this不受with语句影响this始终指向函数调用时的上下文- 在
with块内,this仍然指向原来的对象(通常是全局对象)
-
window 的指向:
- 不能直接用
with(proxyWindow) - 应该用
sandboxContext = { window: proxyWindow }然后with(sandboxContext) - 这样代码中的
window会指向sandboxContext.window(即代理对象)
- 不能直接用
-
作用域链查找顺序:
- 局部变量 →
with指定的对象 → 外层作用域 → 全局作用域
- 局部变量 →
-
实际应用:
- 微前端沙箱实现的核心机制
- 通过
with语句替换window引用 - 结合
Proxy实现完全隔离
关键代码模式
javascript
// 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);