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

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

巧合的是,最近受 git chat / git book 邀请,做了一个分享。
其中谈到JS中冻结一个对象几种由浅入深的实践。想想也暗合国学大师所谓的三重境界。 具体分享内容,还没有上车的同学们可以点击这里,参看实录。欢迎一起讨论。相关文章,点击这里也可以查看。

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

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

等等。

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

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

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

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

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

总结下就是设置:

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仓库,欢迎通过代码各种形式交流。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容