美文网首页Web前端之路让前端飞程序员
JS冻结对象的《人间词话》 完美实现究竟有几层境界?

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

作者: LucasHC | 来源:发表于2017-06-06 17:27 被阅读871次

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

    巧合的是,最近受 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仓库,欢迎通过代码各种形式交流。

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/mnogfxtx.html