王国维讲述了他在《人间词话》的求学经历。他说:“古今有大业大学者,必经三境。”巧合的是,最近受gitchat/gitbook的邀请,做了一个分享。由浅入深讲了JS中冻结一个对象的几种做法。想来也与国学大师所谓的三重境界不谋而合。本文由浅入深地讨论了JS中对象的一些加锁特性。不过都是一些基本语法的实现,相信就算是前端小白也能大致看懂。不过读者需要提前了解JS中对象的特性,尤其是对象自身属性的描述符:可配置、可写……另外,如果你对JS中的对象操作、不可变数据、函数式编程感兴趣,也推荐我的其他一些相关文章:如何优雅安全地获取深层数据结构中的值从JS对象入手,浅谈“不可变数据”与函数式编程,分析推特前端架构,学习复杂场景数据设计,等昨夜西风枯绿树,独上高楼看天涯。第一句:“昨夜西风枯绿树,独上高楼看天涯。”这句话出自晏殊的,原意是,“我”登上高楼,看秋色更凄凉,西风黄叶,山河悠悠,到底是怎么回事?王国维在这句话中解释道:要想成为大学士,首先要有执着的追求,登高望远,看清道路,明确目标和方向,看清事物的大体。让我们从最基本的场景开始,到底为什么要冻结一个对象?场景一:我们造了一个轮子,对外暴露一个对象,开放给第三方使用。同时还要保证这个暴露出来的对象是完全安全的,不能被业务代码覆盖或者hook。场景二:如果你看过Vue2.*版本的源码,你会发现冻结对象的操作频繁发生。我们先来看冻结对象的第一层实现——扩展特性锁:它包含两个基本方法:Object.isExtensibleObject.preventExtensions如果一个对象可以添加新的属性,那么这个对象就是可扩展的。扩展特性锁是让这个对象不可扩展,即不能再有新的属性。MDN上Object.isExtensible的概述:OverviewObject.isExtensible()方法确定对象是否可扩展(是否可以向其添加新属性)。语法Object.isExtensible(obj)参数obj待检测的对象例如我们通常使用对象字面量声明的对象是可扩展的:varperson1={};person1.name="Lucas";console.log(person1);//{name:"Lucas"}同时:Object.isExtensible(person1)===true;//true你可能要问了,那么使用Object.create方法声明对象和配置对象属性是什么情况呢?我们知道用上面的对象字面量声明的对象等价于:varperson1=Object.create({},{"name":{value:"Lucas",configurable:true,//Notconfigurableenumerable:true,//enumerablewritable:true//可写}});甚至尝试将configurable设置为false:varperson1=Object.create({},{"name":{value:"Lucas",configurable:false,//Notconfigurableenumerable:true,//enumerablewritable:true//可写}});仍然得到:Object.isExtensible(person1)===true;//trueObject.preventExtensions当然,我们还有办法让一个对象变得不可扩展。MDN上的内容概述:概述Object.preventExtensions()方法使对象不可扩展,即永远不能添加新属性。语法Object.preventExtensions(obj)参数obj将变为不可扩展的对象需要注意的几点包括但不限于:不可扩展对象的属性通常仍然可以被删除。尝试向不可扩展对象添加新属性将失败,要么是静默的,要么是TypeError(在严格模式下)。Object.preventExtensions只能阻止对象添加新的自身属性,并且仍然可以向对象的原型添加属性。例如:varperson1={name:"Lucas"}Object.preventExtensions(person1);person1.age=18;//在非严格模式下,这里不会报错,是静默失败person1.age//undefined//extension如果新建属性失败,仍然可以在原型链中添加属性:person1.__proto__.age=18;person1.age//18//可以从原型链中获取,也可以重写一些属性:person1.name="Eros";person1.name//"Eros"也可以删除已有的属性:person1.name;//"Eros",删除person1.name;person1.name;//undefined通过上面的方法,我们实现了一个对象属性freeze的扩展。但也承认这不是全面的保护:例如,可以随意更改以覆盖现有属性,并且仍然很难阻止向对象原型链添加属性。第二种境界:“衣带渐宽不悔,人为一笑憔悴。”此句引自北宋刘永《蝶恋花》的最后两句,原话表达了作者在爱情中的艰辛,在爱情中的无悔。如果把“意”字理解为诗人所追求的理想,理解为他一生所从事的事业,那也无妨。王国维别有用心,用这两句话比喻成为伟业,成为大学士。得之不易,得之不易,却须踏踏实实。这是一种更深入的方法:密封功能。密封对象是那些不能增加新的属性,不能删除已有的属性,不能修改已有属性的可枚举、可配置和可写的属性,但可能是可修改属性已经有值的对象。他还包含两个基本方法:Object.isSealedObject.sealObject.isSealedOverviewMDN上的内容:OverviewObject.isSealed()方法判断一个对象是否密封(sealed)。语法Object.isSealed(obj)参数obj待检测对象普通对象字面量声明的对象未密封:varperson1={name:"Lucas"}Object.isSealed(person1);//当对象被密封时为false当扩展被禁止时它也不会被密封:varperson1={name:"Lucas"}Object.preventExtensions(person1);Object.isSealed(person1);//false但最重要的是,使用Object.defineProperty方法,使属性不可配置(可配置),那么这个对象就变成了一个密封对象:varperson1={name:"Lucas"}Object.defineProperty(person1,"name",{configurable:false});Object.isSealed(person1);//true此时,我们有:Object.getOwnPropertyDescriptor(person1,'name');//Get:Object{value:"Lucas",writable:true,enumerable:true,configurable:false}根据这个getOwnPropertyDescriptor,我们可以对密封特性有更深入的理解:密封对象是在不可扩展性的基础上,将可配置的属性描述符设置为false;同时,被封印的对象还有机会改变财产的价值。只是对于这个对象本身,不能扩展新的属性,不能改变已有属性的配置信息。对应于Object.seal,我们也有一个方法来密封一个对象。MDN上的内容概览:OverviewObject.seal()方法可以密封一个对象并返回密封后的对象。语法Object.seal(obj)参数obj被密封的对象例如:varperson1={name:"Lucas"}Object.getOwnPropertyDescriptor(person1,'name');//获取:对象{value:"Lucas",writable:true,enumerable:true,configurable:true}密封这个对象后:Object.seal(person1);Object.getOwnPropertyDescriptor(person1,'name');//get:Object{value:"Lucas",writable:true,enumerable:true,configurable:false}也就是说:person1.age=18;person1.age;//undefined//扩展新属性失败//同时调用defineProperty失败Object.defineProperty(person1,"name",{get:function(){return"g";}});//抛出一个exception除了更改属性值之外的任何操作都将在非严格模式下无声地失败,如上和如下:deleteperson1.name;person1.name;//“Lucas”并改变属性值即可成功:person1.name="Eros";person1.name;//“Eros”是如何理解这种现象的?请记住,密封对象具有以下属性描述符:Object{value:"Lucas",writable:true,enumerable:true,configurable:false}删除属性是可配置的,更改属性是可写的;在此基础上稍微扩展一下,我们其实已经可以完成冻结对象的第三阶段:实现密封,不修改原有的属性值。因为你可以这样做:varperson1={name:"Lucas"};Object.defineProperty(person1,"name",{configurable:false,writable:false});对象.preventExtensions(o);综上,就是setting:configurable:false+writable:false+preventExtensions还是因为configurable:false+preventExtensions=seal,所以也可以set:seal+writable:false我在人群中找了他几千遍,和蓦然回首,人却在,灯火阑珊处。第三境:“大家寻他万遍,蓦然回首,那人却在那里,在灯火阑珊处。”这是南宋辛弃疾《青玉案》诗的最后四句,梁启超称此词为“自怜孤寂,悲人无臂”。这是比喻有事,无事。王国维已经说过,“我们可以毫不费力地涉足。”他把这个词的最后四句作为“境界”的第三个,即最终的最高境界。虽然这不是辛弃疾的本意,也可以引出悠悠的远义:欲学成业者,欲达第三境者,须有定力之精神,反复追求、研究、努力,自然而然。导致清晰的认识。如果你做出一些发现和发明,你就能从必然的境界进入自由的境界。上面冻结对象的方法实际上有一个原生的实现。可以说:“我在人群中找了他几千遍,蓦然回首,那人却在那里,在一个昏暗的地方。数据属性是不可写的。换句话说,冻结对象是指不能添加新属性,不能修改已有属性的值,不能删除已有属性,或者修改已有属性的可枚举性、可配置性和可写性的对象。换句话说,这个对象总是不可变的。类似地,包括两个基本方法:Object.isFrozenObject.freezeObject.isFrozenMDN上的内容概述:OverviewObject.isFrozen()方法判断一个对象是否被冻结(frozen)。语法Object.isFrozen(obj)参数obj待检测的对象语法Object.freeze(obj)参数obj待冻结的对象可以理解为冻结对象的最高层:varperson1={name:"Lucas"}Object.freeze(person1);此时,我们有:Object.getOwnPropertyDescriptor(person1,'name')Object{value:"Lucas",writable:false,enumerable:true,configurable:false}//对冻结对象的任何操作都将失败person1.name="爱神";//重写属性值,非严格模式静默失败;person1.age=18;//扩展属性值,非严格模式静默失败;Object.defineProperty(person1,"name",{value:"Eros"});//使用defineProperty会直接报错重写属性值,扩展新属性,调用defineProperty,都会失败。但是,这种程度的冻结只是浅层冻结。如果对象内部嵌套了对象,那么内部对象根本不受影响。varperson1={name:"Lucas",family:{brother:"Eros"}}Object.freeze(person1);person1.family.brother="Tim";person1.family.brother//"Tim"的最终实现“那么,如果我们想深度冻结一个物体怎么办?思路与深拷贝不谋而合,使用递归:Object.prototype.deepFreeze=Object.prototype.deepFreeze||函数(o){varprop,propKey;对象.冻结(o);//首先冻结第一层对象for(propKeyino){prop=o[propKey];if(!o.hasOwnProperty(propKey)||!(typeofprop==="object")||Object.isFrozen(prop)){继续;}deepFreeze(prop);//递归}}像这样,让我们??回顾一下:varperson1={name:"Lucas",family:{brother:"Eros"}}Object.deepFreeze(person1);person1.family.brother="Tim";person1.family.brother//“Eros”已达到深层对象属性冻结。小结本文介绍了三种冻结对象的高级方法。它们层层递进,却又相互关联。关系如图:文章的一些概念是基于MDN语法的介绍和Tomson的文章。在《文学小言》一文中,王国维将以上三个境界描述为“三等”。并说:“没有一个人不读一、二课就能跳上三课的,文学也一样,有文学天才,所以需要多多培养。”分享给大家。编码愉快!PS:作者的Github仓库,欢迎通过代码以各种形式交流。
