资讯专栏INFORMATION COLUMN

JS冻结对象的《人间词话》 完美实现究竟有几层境界?

YorkChen / 652人阅读

摘要:王国维在人间词话里谈到了治学经验,他说古今之成大事业大学问者,必经过三种之境界。其中谈到中冻结一个对象几种由浅入深的实践。王国维已先自表明,吾人可以无劳纠葛。总结本文先后介绍了关于冻结一个对象的三种进阶方法。

王国维在《人间词话》里谈到了治学经验,他说:“古今之成大事业、大学问者,必经过三种之境界。”

巧合的是,最近受 git chat / git book 邀请,做了一个分享。
其中谈到JS中冻结一个对象几种由浅入深的实践。想想也暗合国学大师所谓的三重境界。

这篇文章由浅入深讨论JS中对象的一些锁定特性。但都是一些基础语法的实现,相信即便是前端小白也可以大体领会。不过需要读者预先了解JS中对象的特性,尤其是对象自身属性的描述符:configurable、writable...

另外,如果您对JS中对象操作、不可变数据、函数式编程感兴趣,同样推荐我的其他一些相关文章:

如何优雅安全地在深层数据结构中取值

从JS对象开始,谈一谈“不可变数据”和函数式编程

解析Twitter前端架构 学习复杂场景数据设计

等等。

昨夜西风凋碧树 独上高楼 望尽天涯路

第一种境界:“昨夜西风凋碧树,独上高楼,望尽天涯路。”
该词句出自晏殊的《蝶恋花》,原意是说,“我”上高楼眺望所见的更为萧飒的秋景,西风黄叶,山阔水长,案书何达?

王国维此句中解成:做学问成大事业者,首先要有执着的追求,登高望远,瞰察路径,明确目标与方向,了解事物的概貌。

我们就从最基本的场景说起,究竟为什么要冻结一个对象?

场景一:
我们造了一个轮子,对外暴露一个对象,开放出来给第三方使用。同时需要保证这个对外暴露的对象完全安全,不能被业务代码所改写覆盖或下钩子(hook)函数。

场景二:
如果你看过Vue 2.* 版本源码,你会发现冻结一个对象的操作频繁出现。

我们先来看冻结对象的第一层实现 —— 扩展特性锁:

他包含了两个基本方法:

Object.isExtensible

Object.preventExtensions

如果一个对象可以添加新的属性,则这个对象是可扩展的。扩展特性锁就是让这个对象变的不可扩展,也就是不能再有新的属性。

Object.isExtensible

MDN上内容概述:

概述
    Object.isExtensible() 方法判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)。
语法
    Object.isExtensible(obj)
参数
    obj 需要检测的对象

例如,我们正常使用对象字面量声明的对象都是可扩展的:

var person1 = {};
person1.name = "Lucas";
console.log(person1);
// {name: "Lucas"}

同时:

Object.isExtensible(person1) === true; // true

你可能要问了,那么使用Object.create方法声明的对象,并对该对象属性进行配置是什么情况呢?
我们知道,用上面对象字面量声明的对象相当于:

var person1 = Object.create({},{
    "name":{
        value : "Lucas",
        configurable : true, //不可配置
        enumerable : true , //可枚举
        writable : true //可写
    }
});

即便尝试将configurable设置为false:

var person1 = Object.create({},{
    "name":{
        value : "Lucas",
        configurable : false, //不可配置
        enumerable : true, //可枚举
        writable : true //可写
    }
});

仍然得到:

Object.isExtensible(person1) === true; // true
Object.preventExtensions

当然,我们还是有方法可以使得一个对象变的不可扩展。

MDN上内容概述:

概述
    Object.preventExtensions() 方法让一个对象变的不可扩展,也就是永远不能再添加新的属性。
语法
    Object.preventExtensions(obj)
参数
    obj 将要变得不可扩展的对象

几个注意点包括但不限于:

不可扩展的对象的属性通常仍然可以被删除。

尝试给一个不可扩展对象添加新属性的操作将会失败,不过可能是静默失败,也可能会抛出 TypeError 异常(严格模式下)。

Object.preventExtensions 只能阻止一个对象不能再添加新的自身属性,仍然可以为该对象的原型添加属性。

比如:

var person1 = {
    name: "Lucas"
}

Object.preventExtensions(person1);
person1.age = 18;
// 非严格模式下,这里不会有报错,属于静默失败
person1.age // undefined
// 扩展新属性失败了

仍然可以向原型链添加属性:

person1.__proto__.age = 18;
person1.age // 18
// 可以从原型链上取到

同样也可以复写一些属性:

person1.name = "Eros";
person1.name // "Eros"

也可以删除已有属性:

person1.name; //  "Eros",
delete person1.name;
person1.name; // undefined

通过以上方法,我们实现了对一个对象属性扩展的冻结。但是同样也认识到,这并不是全面的保护:例如可以随意改动去覆盖已有属性,在对象原型链上增加属性也还是难以屏蔽。

衣带渐宽终不悔 为伊消得人憔悴

第二种境界:“衣带渐宽终不悔,为伊消得人憔悴。”
这引用的是北宋柳永《蝶恋花》最后两句词,原词是表现作者对爱的艰辛和爱的无悔。若把“伊”字理解为词人所追求的理想和毕生从事的事业,亦无不可。王国维则别有用心,以此两句来比喻成大事业、大学问者,不是轻而易举,随便可得的,必须坚定不移,经过一番辛勤劳动,废寝忘食,孜孜以求,直至人瘦带宽也不后悔。

下面介绍一个更深一层的做法:密封特性。

密封对象是指那些不能添加新的属性,不能删除已有属性,以及不能修改已有属性的可枚举性(enumerable)、可配置性(configurable)、可写性(writable),但可能可以修改已有属性的值的对象。

他同样包含了两个基本方法:

Object.isSealed

Object.seal

Object.isSealed

MDN上内容概述:

概述 
    Object.isSealed() 方法判断一个对象是否是密封的(sealed)。
语法 
    Object.isSealed(obj)
参数
    obj 将要检测的对象

正常对象字面量声明的对象是不被密封的:

var person1 = {
    name: "Lucas"
}

Object.isSealed(person1); // false

当将这个对象禁止扩展时,它也不会变成密封的:

var person1 = {
    name: "Lucas"
}
Object.preventExtensions(person1);
Object.isSealed(person1); // false

但是在此基础上,使用Object.defineProperty方法,把属性变得不可配置(configurable),则这个对象也就成了密封对象:

var person1 = {
    name: "Lucas"
}

Object.defineProperty(person1, "name", {configurable : false});

Object.isSealed(person1); // true

此时,我们有:

Object.getOwnPropertyDescriptor(person1, "name");
// 得到:
Object {
    value: "Lucas", 
    writable: true, 
    enumerable: true, 
    configurable: false
}

根据这个getOwnPropertyDescriptor,我们可以更加深入的理解密封特性:被密封的对象,就是在不可扩展基础上讲属性描述符configurable设置为false; 同时,被密封的对象,仍然有机会改变属性的值。只不过对于此对象本身而言,不可以再扩展新的属性,不可以更改已有属性的配置信息。

Object.seal

相对应我们也有一个方法将一个对象密封。

MDN上内容概述:

概述
    Object.seal() 方法可以让一个对象密封,并返回被密封后的对象。
语法
    Object.seal(obj)
参数
    obj 将要被密封的对象

比如:

var person1 = {
    name: "Lucas"
}
Object.getOwnPropertyDescriptor(person1, "name");
// 得到:
Object {
    value: "Lucas", 
    writable: true, 
    enumerable: true, 
    configurable: true
}

将此对象密封后:

Object.seal(person1);
Object.getOwnPropertyDescriptor(person1, "name");
// 得到:
Object {
    value: "Lucas", 
    writable: true, 
    enumerable: true, 
    configurable: false
}

也就是说:

person1.age = 18;
person1.age; // undefined
// 扩展新属性失败

// 同时调用defineProperty失败
Object.defineProperty(person1,"name",{get : function(){return "g";}});
// 抛出异常

任何除更改属性值以外的操作,非严格模式下都会静默失败,如上并如下:

delete person1.name;
person1.name; // "Lucas"

而更改属性值可以成功:

person1.name = "Eros";
person1.name; // "Eros"

怎么理解这样的现象呢?牢记,被密封的对象拥有如下的属性描述符:

Object {
    value: "Lucas", 
    writable: true, 
    enumerable: true, 
    configurable: false
}

而删除属性属于configurable,更改属性才属于writable;

一点延伸

借助于此,我们其实已经可以完成冻结对象的第三重境界:达到即密封又不可修改原属性值。因为可以这样做:

var person1 = {name: "Lucas"};
Object.defineProperty(person1, "name", {configurable: false, writable: false});
Object.preventExtensions(o);

总结下就是设置:

configurable: false + writable: false + preventExtensions

或者因为

configurable: false+ preventExtensions = seal

所以也可以设置:

seal + writable: false

众里寻他千百度,蓦然回首,那人却在,灯火阑珊处

第三种境界:“众里寻他千百度,蓦然回首,那人却在,灯火阑珊处。”
这是引用南宋辛弃疾《青玉案》词中的最后四句。梁启超称此词“自怜幽独,伤心人别有怀抱”。这是借词喻事,与文学赏析已无交涉。王国维已先自表明,“吾人可以无劳纠葛”。他以此词最后的四句为“境界”之第三,即最终最高境界。

这虽不是辛弃疾的原意,但也可以引出悠悠的远意:做学问、成大事业者,要达到第三境界,必须有专注的精神。反复追寻、研究,下足功夫,自然会豁然贯通,有所发现,有所发明,就能够从必然王国进入自由王国。

上边那种冻结对象的方法,其实也有原生实现,可谓:“众里寻他千百度,蓦然回首,那人却在,灯火阑珊处”

我们这里所说的一个对象的冻结(frozen)是指它不可扩展,所有属性都是不可配置的(non-configurable),且所有数据属性(data properties)都是不可写的(non-writable)。

或者说,冻结对象是指那些不能添加新的属性,不能修改已有属性的值,不能删除已有属性,以及不能修改已有属性的可枚举性、可配置性、可写性的对象。也就是说,这个对象永远是不可变的。

同样,包含了两个基本方法:

Object.isFrozen

Object.freeze

Object.isFrozen

MDN上内容概述:

概述
    Object.isFrozen() 方法判断一个对象是否被冻结(frozen)。
语法
    Object.isFrozen(obj)
参数
    obj 被检测的对象
Object.freeze 方法

MDN上内容概述:

概述
    Object.freeze() 方法可以冻结一个对象。
语法
    Object.freeze(obj)
参数
    obj 将要被冻结的对象

可以先理解为,这是最高一层的冻结对象:

var person1 = {
    name: "Lucas"
}

Object.freeze(person1);

此时,我们有:

Object.getOwnPropertyDescriptor(person1, "name")

Object {
    value: "Lucas", 
    writable: false, 
    enumerable: true, 
    configurable: false
}

// 对冻结对象的任何操作都会失败
person1.name = "Eros"; // 改写属性值,非严格模式下静默失败;
person1.age = 18; // 扩展属性值,非严格模式下静默失败;
Object.defineProperty(person1,"name",{value: "Eros"}); // 使用defineProperty会直接报错

改写属性值,扩展新属性,调用defineProperty,全部都会失败。

但是,这种层面的冻结,只是浅冻结。如果对象里面还嵌套有对象,那么这个内部对象丝毫不受影响。

var person1 = {
    name: "Lucas",
    family: {
        brother: "Eros"
    }
}

Object.freeze(person1);

person1.family.brother = "Tim";
person1.family.brother // "Tim"
终极实现

那么,如果我们想深层次冻结一个对象呢?思路和深拷贝暗合,使用递归:

Object.prototype.deepFreeze = Object.prototype.deepFreeze || function (o){
    var prop, propKey;
    Object.freeze(o); // 首先冻结第一层对象
    for (propKey in o){
        prop = o[propKey];
        if(!o.hasOwnProperty(propKey) || !(typeof prop === "object") || Object.isFrozen(prop)){
            continue;
        }
        deepFreeze(prop); // 递归
    }
}

这样子,我们再回过头来看:

var person1 = {
    name: "Lucas",
    family: {
        brother: "Eros"
    }
}

Object.deepFreeze(person1);

person1.family.brother = "Tim";
person1.family.brother // "Eros"

已经达到了深层次对象属性的冻结。

总结

本文先后介绍了关于冻结一个对象的三种进阶方法。他们层层递进,却又相互关联。关系如图:

文章部分概念粘取了MDN语法介绍和Tomson的文章。

在《文学小言》一文中,王国维把上述三境界说成“三种之阶级”。并说:“未有不阅第一第二阶级而能遽跻第三阶级者,文学亦然。此有文学上之天才者,所以又需莫大之修养也。”

与大家共勉。

Happy coding!

PS: 作者Github仓库,欢迎通过代码各种形式交流。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/83378.html

相关文章

  • JS冻结对象人间词话完美实现究竟几层境界

    摘要:王国维在人间词话里谈到了治学经验,他说古今之成大事业大学问者,必经过三种之境界。其中谈到中冻结一个对象几种由浅入深的实践。王国维已先自表明,吾人可以无劳纠葛。总结本文先后介绍了关于冻结一个对象的三种进阶方法。 王国维在《人间词话》里谈到了治学经验,他说:古今之成大事业、大学问者,必经过三种之境界。 巧合的是,最近受 git chat / git book 邀请,做了一个分享。其中谈到J...

    高胜山 评论0 收藏0
  • 2017-06-07 前端日报

    摘要:程序员英语提升指南深入理解内部机制即将到来,现在该考虑新的打包方案了嘛冻结对象的人间词话完美实现究竟有几层境界关于响应式的另一种思考 程序员英语提升指南 深入理解 Node Stream 内部机制 ES6 modules 即将到来,现在该考虑新的打包方案了嘛? JS 冻结对象的《人间词话》 完美实现究竟有几层境界? 关于响应式的另一种思考 Best websites a program...

    rose 评论0 收藏0
  • 「能写代码」是愚公移山,「会写代码」是女娲补天

    摘要:从能力上分,一个是搬运工,一个是设计者能写代码是愚公移山为什么说能写代码是愚公移山呢我们中国大部分程序员都应该处于一个初级程序员的水平,怎么讲只有少数的程序员处于中高级水平。 导语:你知道普通程序员和优秀程序员之间的差距吗?其实答案很简单,那就是「愚公移山」和「女娲补天」之间的区别。 之所以提这个话题,跟前两天在微信群里的讨论有关,年后本该是跳槽、找工作的高峰月份,各公司面试邀约应该很...

    ghnor 评论0 收藏0
  • 迁移学习在图像分类中简单应用策略

    摘要:地址为什么使用迁移学习根据联合创始人斯坦福副教授吴恩达介绍,迁移学习将会成为机器学习商业成就的下一驱动力。迁移学习是一种机器学习技术,允许在特定的数据集上再利用已训练的卷积神经网络,并将其调整或迁移到其他数据集。 GitHub 地址:https://github.com/miguelgfierro/sciblog_support/blob/master/A_Gentle_Introducti...

    BigNerdCoding 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<