31、JavaScript 对象属性、保护机制与代理全解析

JavaScript 对象属性、保护机制与代理全解析

1. 对象属性特性

在处理对象属性时,我们通常知道它们有键(可以是字符串或符号)和值(可以是任何类型)。但实际上,对象属性还有一些特性,这些特性控制着属性在所属对象上下文中的行为。

1.1 属性特性概述

属性特性、属性描述符和属性配置这几个术语可以互换使用,它们都指的是同一件事。属性有三个主要特性:
- 可写性(Writable) :控制属性的值是否可以更改。
- 可枚举性(Enumerable) :控制在枚举对象属性时(使用 for...in Object.keys 或扩展运算符),该属性是否会被包含在内。
- 可配置性(Configurable) :控制属性是否可以从对象中删除,或者其特性是否可以修改。

我们可以通过一个简单的例子来查看属性的特性:

const obj = { foo: "bar" };
const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');
console.log(descriptor); 
// 输出: { value: "bar", writable: true, enumerable: true, configurable: true }

1.2 控制属性特性

我们可以使用 Object.defineProperty 来控制属性的特性,它允许我们创建新属性或修改现有属性(只要该属性是可配置的)。

1.2.1 使属性只读
Object.defineProperty(obj, 'foo', { writable: false });
try {
    obj.foo = 3;
} catch (error) {
    console.log(error); 
    // 输出: TypeError: Cannot assign to read only property 'foo' of [object Object]
}

需要注意的是,尝试设置只读属性只会在严格模式下导致错误。在非严格模式下,赋值不会成功,但不会有错误。

1.2.2 添加新属性
Object.defineProperty(obj, 'color', {
    get: function() { return this.color; },
    set: function(value) { this.color = value; },
});
1.2.3 创建数据属性
Object.defineProperty(obj, 'name', {
    value: 'Cynthia',
});
Object.defineProperty(obj, 'greet', {
    value: function() { return `Hello, my name is ${this.name}!`; }
});
1.2.4 使数组属性不可枚举
const arr = [3, 1.5, 9, 2, 5.2];
arr.sum = function() { return this.reduce((a, x) => a+x); }
arr.avg = function() { return this.sum()/this.length; }
Object.defineProperty(arr, 'sum', { enumerable: false });
Object.defineProperty(arr, 'avg', { enumerable: false });

我们也可以一步完成:

const arr = [3, 1.5, 9, 2, 5.2];
Object.defineProperty(arr, 'sum', {
    value: function() { return this.reduce((a, x) => a+x); },
    enumerable: false
 });
Object.defineProperty(arr, 'avg', {
    value: function() { return this.sum()/this.length; },
    enumerable: false
});

还可以使用 Object.defineProperties 来批量定义属性:

const arr = [3, 1.5, 9, 2, 5.2];
Object.defineProperties(arr, {
    sum: {
        value: function() { return this.reduce((a, x) => a+x); },
        enumerable: false
     },
    avg: {
        value: function() { return this.sum()/this.length; },
        enumerable: false
    }
});

2. 保护对象:冻结、密封和防止扩展

JavaScript 的灵活性虽然强大,但也可能带来问题。为了防止意外修改(并使有意修改更困难),JavaScript 提供了三种机制:冻结、密封和防止扩展。

2.1 冻结对象

冻结对象可以防止对对象进行任何更改。一旦冻结对象,就不能:
- 设置对象属性的值。
- 调用修改对象属性值的方法。
- 调用对象上的 setter(修改对象属性值的)。
- 添加新属性。
- 添加新方法。
- 更改现有属性或方法的配置。

const appInfo = {
    company: 'White Knight Software, Inc.',
    version: '1.3.5',
    buildId: '0a995448-ead4-4a8b-b050-9c9083279ea2',
    copyright() {
        return `© ${new Date().getFullYear()}, ${this.company}`;
    },
};
Object.freeze(appInfo);
console.log(Object.isFrozen(appInfo)); 
try {
    appInfo.newProp = 'test';
} catch (error) {
    console.log(error); 
    // 输出: TypeError: Can't add property newProp, object is not extensible
}
try {
    delete appInfo.company;
} catch (error) {
    console.log(error); 
    // 输出: TypeError: Cannot delete property 'company' of [object Object]
}
try {
    appInfo.company = 'test';
} catch (error) {
    console.log(error); 
    // 输出: TypeError: Cannot assign to read-only property 'company' of [object Object]
}
try {
    Object.defineProperty(appInfo, 'company', { enumerable: false });
} catch (error) {
    console.log(error); 
    // 输出: TypeError: Cannot redefine property: company
}

2.2 密封对象

密封对象可以防止添加新属性,或重新配置或删除现有属性。

class Logger {
    constructor(name) {
        this.name = name;
        this.log = [];
    }
    add(entry) {
        this.log.push({
            log: entry,
            timestamp: Date.now(),
        });
    }
}
const log = new Logger("Captain's Log");
Object.seal(log);
console.log(Object.isSealed(log)); 
log.name = "Captain's Boring Log"; 
log.add("Another boring day at sea...."); 
try {
    log.newProp = 'test';
} catch (error) {
    console.log(error); 
    // 输出: TypeError: Can't add property newProp, object is not extensible
}
try {
    delete log.name;
} catch (error) {
    console.log(error); 
    // 输出: TypeError: Cannot delete property 'name' of [object Object]
}
try {
    Object.defineProperty(log, 'log', { enumerable: false });
} catch (error) {
    console.log(error); 
    // 输出: TypeError: Cannot redefine property: log
}

2.3 防止对象扩展

防止对象扩展只会阻止添加新属性,属性仍然可以被赋值、删除和重新配置。

const log2 = new Logger("First Mate's Log");
Object.preventExtensions(log2);
console.log(Object.isExtensible(log2)); 
log2.name = "First Mate's Boring Log"; 
log2.add("Another boring day at sea...."); 
try {
    log2.newProp = 'test';
} catch (error) {
    console.log(error); 
    // 输出: TypeError: Can't add property newProp, object is not extensible
}
log2.name = 'test'; 
delete log2.name; 
Object.defineProperty(log2, 'log', { enumerable: false }); 

2.4 对象保护选项总结

操作 普通对象 冻结对象 密封对象 不可扩展对象
添加属性 允许 阻止 阻止 阻止
读取属性 允许 允许 允许 允许
设置属性值 允许 阻止 允许 允许
重新配置属性 允许 阻止 阻止 允许
删除属性 允许 阻止 阻止 允许

2.5 对象保护机制流程图

graph TD;
    A[普通对象] -->|冻结| B(冻结对象);
    A -->|密封| C(密封对象);
    A -->|防止扩展| D(不可扩展对象);
    B -->|不可进行多种操作| E(属性和方法不可更改);
    C -->|不可添加、重新配置或删除属性| F(部分操作受限);
    D -->|不可添加新属性| G(属性仍可赋值、删除和重新配置);

3. 代理(Proxies)

ES6 中引入了代理,它提供了额外的元编程功能(元编程是指程序能够修改自身的能力)。

3.1 代理基础

对象代理本质上能够拦截并(可选地)修改对对象的操作。我们通过一个简单的例子来演示如何修改属性访问。

const coefficients = {
    a: 1,
    b: 2,
    c: 5,
};
function evaluate(x, c) {
    return c.a + c.b * x + c.c * Math.pow(x, 2);
}
const betterCoefficients = new Proxy(coefficients, {
    get(target, key) {
        return target[key] || 0;
    },
});
console.log(betterCoefficients.a); 
console.log(betterCoefficients.b); 
console.log(betterCoefficients.c); 
console.log(betterCoefficients.d); 
console.log(betterCoefficients.anything); 

Proxy 构造函数的第一个参数是目标对象,即被代理的对象。第二个参数是处理程序,它指定要拦截的操作。在这个例子中,我们只拦截属性访问,由 get 函数表示。

3.2 进一步修改代理

我们可以进一步修改代理,使其只代理单个小写字母:

const betterCoefficients = new Proxy(coefficients, {
    get(target, key) {
        if(!/^[a-z]$/.test(key)) return target[key];
        return target[key] || 0;
    },
});

3.3 拦截属性设置

我们也可以使用 set 处理程序来拦截属性(或访问器)的设置。

const cook = {
    name: "Walt",
    redPhosphorus: 100, 
    water: 500, 
};
const protectedCook = new Proxy(cook, {
    set(target, key, value) {
        if(key === 'redPhosphorus') {
            if(target.allowDangerousOperations)
                return target.redPhosphorus = value;
            else
                return console.log("Too dangerous!");
        }
        target[key] = value;
    },
});
protectedCook.water = 550; 
protectedCook.redPhosphorus = 150; 
protectedCook.allowDangerousOperations = true;
protectedCook.redPhosphorus = 150; 

4. 学习资源推荐

4.1 在线文档

  • Mozilla Developer Network (MDN) :JavaScript、CSS 和 HTML 文档的首选资源。
  • Mark Pilgrim 的 Dive Into HTML5 :适合新手学习 HTML5。
  • WHATWG 的 HTML5 规范 :对于难以回答的 HTML 问题很有帮助。
  • W3C 网站的 HTML 和 CSS 官方规范 :虽然文档枯燥难读,但对于解决难题是最后的手段。
  • ES6 遵循的 ECMA - 262 ECMAScript 2015 语言规范 :可参考 @kangax 维护的指南来跟踪 ES6 特性在 Node 和各种浏览器中的可用性。
  • jQuery 和 Bootstrap 的在线文档 :非常优秀。
  • Node 文档 :高质量且全面,是 Node 模块的权威文档。
  • npm 文档 :特别是关于 package.json 文件的页面很有用。

4.2 期刊

  • JavaScript Weekly
  • Node Weekly
  • HTML5 Weekly

4.3 博客和教程

  • Axel Rauschmayer 的博客 :关于 ES6 和相关技术的优秀文章。
  • Nolan Lawson 的博客 :有很多关于实际 JavaScript 开发的详细文章。
  • David Walsh 的博客 :关于 JavaScript 开发和相关技术的精彩文章。
  • @kangax 的博客,Perfection Kills :充满了精彩的教程、练习和测验。

4.4 在线课程和教程

  • Lynda.com JavaScript 教程
  • Treehouse JavaScript 轨道
  • Codecademy 的 JavaScript 课程
  • Microsoft Virtual Academy 的 JavaScript 入门课程

4.5 Stack Overflow 使用建议

Stack Overflow 是程序员获取 JavaScript 问题答案的最佳资源。为了增加问题得到有用答案的机会,可以采取以下措施:
1. 成为知情的 SO 用户 :参加 SO 之旅,阅读 “How do I ask a good question?”,如果愿意,可以阅读所有帮助文档。
2. 不重复提问 :在提问前先进行充分的搜索,避免提出已有答案的问题。
3. 不要求别人代写代码 :在寻求帮助之前,先尝试自己解决问题,并在问题中描述自己的尝试和遇到的问题。
4. 一次只问一个问题 :避免提出包含多个问题的复杂问题。
5. 构建最小示例 :避免粘贴大量代码,构建最小示例有助于自己调试和提高批判性思维能力。
6. 学习 Markdown :Stack Overflow 使用 Markdown 格式化问题和答案,学习 Markdown 可以提高问题的可读性。
7. 接受和点赞答案 :如果有人满意地回答了你的问题,应该点赞并接受答案,以提高回答者的声誉。
8. 自己回答问题 :如果自己解决了问题,为了社区的利益,可以自己回答问题。

如果享受帮助社区的过程,可以考虑自己回答问题。如果问题两天内没有得到有用的答案,可以使用自己的声誉发起悬赏。

5. 总结与应用场景分析

5.1 特性总结

  • 对象属性特性 :对象属性除了键值对,还有可写性、可枚举性和可配置性三个特性,通过 Object.getOwnPropertyDescriptor 查看,使用 Object.defineProperty Object.defineProperties 进行控制。
  • 对象保护机制 :JavaScript 提供了冻结、密封和防止扩展三种机制来保护对象,防止意外修改。
  • 代理 :ES6 引入的代理提供了元编程功能,可拦截并修改对对象的操作。

5.2 应用场景分析

技术 应用场景
对象属性特性 当需要控制属性的读写权限、枚举性或配置时,如创建只读属性、不可枚举属性等。
对象保护机制 当需要确保对象的属性和方法不被意外或恶意修改时,如存储程序的不可变信息。
代理 当需要对对象的属性访问或设置进行额外处理时,如处理缺失属性、防止危险操作等。

5.3 综合应用示例

下面是一个综合应用对象属性特性、保护机制和代理的示例:

// 创建一个对象
const user = {
    name: 'John',
    age: 30,
    email: 'john@example.com'
};

// 使用 Object.defineProperty 设置属性特性
Object.defineProperty(user, 'age', {
    writable: false,
    enumerable: false
});

// 冻结对象
Object.freeze(user);

// 创建代理
const userProxy = new Proxy(user, {
    get(target, key) {
        if (key === 'email') {
            // 对邮箱进行脱敏处理
            return target[key].replace(/(\w{1})\w+(\w{1}@\w+\.\w+)/, '$1***$2');
        }
        return target[key];
    },
    set(target, key, value) {
        console.log('对象已冻结,不能设置属性');
        return false;
    }
});

// 测试
console.log(userProxy.name); 
console.log(userProxy.age); 
console.log(userProxy.email); 
userProxy.age = 31; 

5.4 操作流程总结

graph LR;
    A[创建对象] --> B[设置属性特性];
    B --> C[保护对象];
    C --> D[创建代理];
    D --> E[使用对象和代理];

6. 未来发展趋势与展望

6.1 技术发展趋势

  • 代理的广泛应用 :随着 ES6 的普及,代理的应用场景将越来越广泛,如实现数据绑定、拦截器等。
  • 对象保护机制的强化 :未来可能会有更多的对象保护机制或更精细的控制方式,以满足不同的安全需求。
  • 与其他技术的融合 :对象属性特性、保护机制和代理可能会与其他 JavaScript 技术(如异步编程、模块化等)更紧密地结合,提供更强大的功能。

6.2 学习建议

  • 持续学习 :JavaScript 是一个不断发展的语言,需要持续学习新的特性和技术。
  • 实践项目 :通过实际项目来应用所学的知识,加深理解和掌握。
  • 参与社区 :参与 JavaScript 社区,与其他开发者交流经验和学习心得。

6.3 总结

通过对 JavaScript 对象属性特性、保护机制和代理的学习,我们可以更好地控制对象的行为,提高代码的安全性和可维护性。在实际开发中,我们应该根据具体的需求选择合适的技术,灵活运用这些特性和机制。同时,我们也要关注技术的发展趋势,不断学习和进步,以适应不断变化的开发需求。

希望本文对你理解 JavaScript 的这些重要特性有所帮助,如果你有任何问题或建议,欢迎在评论区留言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值