重新认识对象之对象原生方法学习
前言
谈笑间,樯橹灰飞烟灭。不好意思,走错片场了。(笑) 时间飞逝啊,算上实习的时间,差不多也出来一年了。2017年收获了很多,也认识了很多不足,新年自当不留余力,上下求索。让我重新认识到原生知识的重要性是因为使用NodeJS做后端期间,为了加深对一些原理的理解。于是我开始了看源码计划,看了http模块,以及express依赖的封装实现。虽能力有限,依然颇有所得,倍感欣慰。其中的一些原生方法让我看起来有些吃力,因此决定打好基础,毕竟万丈高楼平地起。(又抱起了JS权威指南)
所获所得
Object.create方法 - 创建对象
ES5定义了一个名为Object.create()的方法,它创建一个的对象,其中第一个参数是这个对象的原型,第二个参数是可选参数,用以对对象的属性进行进一步描述。
· 使用方法:
var o1 = Object.create({x: 1, y: 2}); //只有参数1的情况 console.log(o1.x) // 会输出1 var o2 = Object.create(null); //创建没有原型的新对象 var o3 = Object.create(Object.prototype) // o3同{}和new Object()一样 //Object.create的实现类似同下 function create(o) { let F = function() {}; F.prototype = o; return new F() }
o.hasOwnPreperty方法 - 检测属性
in运算符: 对象的自有属性或继承属性中包含某个属性,则返回true。
hasOwnPreperty方法检测给定的名字是否是对象的自有属性,对于继承的属性返回false
var o = {x: 1}; o.hasOwnProperty("x"); //true o.hasOwnProperty("y"); //false 不存在属性 o.hasOwnProperty("toString"); //false 继承属性
o.propertyIsEnumerable方法 - 检测属性
该方法是 hasOwnProperty方法的增强版,只有检测到是自有属性且这个属性的可枚举性(enumerable attribute)为true时它才返回true。某些内置属性是不可枚举的,通常JS创建的属性都是可枚举的,除非在ES5中使用一个特殊的方法来改变属性的可枚举性(后面提到)
var o = Object.create({x: 2}); o.y = 1; o.propertyIsEnumerable("x"); //true 可枚举自有属性 o.propertyIsEnumerable("y"); //false 继承属性 Object.prototype.propertyIsEnumerable("toString") //false 不可枚举
枚举属性
for/in循环可以在循环体中遍历对象中所有可枚举的属性(包括自有属性和继承的属性),把属性名称赋值给循环变量。对象继承的内置方法不可枚举的,但在代码中给对象添加的属性都是可枚举的(除一个特殊方法可将它们转换为不可枚举的)
Object.keys()返回一个数组,数组由对象中可枚举的自有属性名称组成。(我使用的非常多,就不举例了)
Object.getOwnPropertyNames()方法返回对象的所有自有属性的名称,而不仅仅是可枚举的属性。
Object.getOwnPropertyDescriptor
该方法可以获得某个对象特定属性的属性描述符,且只能得到自有属性的描述符。(想要获得继承属性的特性,需要遍历原型链Object.getPrototypeOf())
首先介绍下什么是属性描述符: ES5中定义了一个名为"属性描述符"的对象,描述符对象的属性和它们所描述的属性特性是同名的。因此,数据属性的描述符对象的属性有value, writable, enumerable, configurable。 存取器属性的描述符对象则用get属性和set属性代替value, writable。 其中writable, enumerable, configurable都是布尔值,当然,get属性和set属性都是函数值。
Object.getOwnPropertyDescriptor({x:1}, "x"); // {value: 1, writable: true, enumerable: true, configurable: true} var random = { get otcet() { return Math.floor(Math.random()*256); } } //给对象直接量定义存取器属性 Object.getOwnPropertyDescriptor(random, "otcet"); //{get: /* func */, set: undefined, enumerable: true, configurable: true}; Object.getOwnPropertyDescriptor({},"x"); //undefined没有这个属性 Object.getOwnPropertyDescriptor({},"toString"); //undefined 继承属性
Object.defineProperty
要想设置属性的特性,或者想让新建属性具有某种特性,则需要调用该方法,传入要修改的对象,要创建或修改的属性的名称以及属性描述符对象。
var o = {}; //添加一个不可枚举的数据属性x, 并赋值为1 Object.defineProperty(o, ”x“, { value: 1, writable: true, enumerable: false, configurable: true }); //属性存在,但不可枚举 o.x // 1 Object.keys(o); // [] //对属性x做修改,让它变为只读 Object.defineProperty(o, "x", { writable: false }); //试图更改这个属性的值 o.x = 2 //失败但是不报错,严格模式中抛出类型错误异常 o.x // 1 //属性依然是可配置的,可通过该方式对它进行修改 Object.defineProperty(o, "x", {value: 2}); //现在将x从数据属性修改为存取器属性 Object.defineProperty(o, "x", { get: function() { return 0; } }); o.x // 0
c传给Object.defineProperty()的属性描述符对象不必包含所有4个特性。对于新创建的属性来说,默认的特性值是false或undefined。对于修改的已有属性来说,默认的特性值没有做任何修改。注意:这个方法要么修改已有属性要么新建自有属性,但不能修改继承属性。
Object.defineProperties
同时修改或创建多个属性,第一个参数是要修改的对象,第二个参数是一个映射表 ,它包含要新建或修改的属性的名称,以及它们的属性描述符。
var p = Object.defineProperty({}, { x: { value: 1, writable: true, enumerable: true, configurable: true }, y: { value: 1, writable: true, enumerable: true, configurable: true }, r: { get: function() { return 0; }, enumerable: true, configurable: true } })
从一个空对象开始,然后给它添加两个数据属性和一个只读存取器属性,最终返回修改后的对象。
Object.getPrototypeOf - 原型属性
ES5中该方法将对象作为参数传入可以查询它的原型。而在ES3中则没有与之等价的函数,但经常通过o.constructor.prototype来检测一个对象的原型。但是使用o.constructor.prototype来检测对象原型的方式并不可靠,注意: 通过对象直接量或Object.create()创建的对象包含一个名为constructor的属性,这个属性指代Object()构造函数。因此,constructor.prorotype才是对象直接量的真正的原型,但对于通过Object.create()创建的对象则往往不是这样的。
稍微解释下原因: (9.2.2) 构造函数的原型中存在预先定义好的constructor属性,这意味着对象通常继承的constructor均指代它们的构造函数。这个涉及到构造函数,原型以及实例见的关系,类似于构成一个环 (你中有我,我中有你)
var a = {}; console.log(a.constructor.prototype === Object.prototype) //true console.log(a.constructor === Object) //true console.log(a.constructor === Object.prototype.constructor) //true
而Object.create()会破坏这种环的关系,因为当你使用某个对象作为原型时,某个对象是没有设置构造函数的反向引用的,会破坏环的关系,如
var b = Object.create({x:1}); Object.getPrototypeOf(b); // 输出原型 {x:1} //补救的方法 显示设置构造函数反向引用 var c = Object.create({x:1,constructor: Object}); console.log(c.constructor === Object); // true console.log(c.constructor.prototype === Object.prototype) //true console.log(a.constructor === Object.prototype.constructor) //true
o.isPrototypeOf - 原型属性
想要检测一个对象是否是另一个对象的原型(或处于原型链中),可使用该方法。
var p = {x: 1}; //定义一个原型对象 var o = Object.create(p); //使用这个原型链创建一个对象 p.isPrototypeOf(o); //true o继承自p Object.prototype.isPrototypeOf(o); //true p继承自Object.prototype
注: isPrototypeOf()函数实现的功能和instance运算符非常相似。
可扩展性
这一部分感觉用的比较少,就不举例说明了。ES5定义了用来查询和设置对象可扩展性的函数。
Object.esExtensible方法: 通过传入对象,来判断该对象是否是可扩展的。
Object.preventExtensions方法: 传入待转换的对象,将对象转换为不可扩展的。注意: 一旦将对象转换为不可扩展的,就无法再将其转换回可扩展的了。同时,preventExtensions只影响到对象本身的可扩展性。
Object.seal方法: 与preventExtensions方法类似,除了能够将对象设置为不可扩展的,还可以将对象的所有自有属性都设置为不可配置的。对于那些已经封闭(sealed)起来的对象是不能解封的。可以使用Object.isSealed()来检测对象是否封闭。
Object.freeze方法: 将更严格地锁定对象--‘冻结’
Object.isFrozen方法: 用来检测对象是否冻结。
序列化对象
对象序列化: 指将对象的状态转换为字符串,也可将字符串还原为对象。
ES5内置函数JSON.parse()和JSON.stringify()用来还原和序列化JS对象的,其中二者都接受第二个可选参数,通过传入需要的序列化或还原的属性列表来定制自定义的序列化或还原操作。可参考MDN说明
两个很特殊的对象方法
toString()方法: 无参数,返回一个表示调用这个方法的对象值的字符串。在需要将对象转换为字符串的时候,JS会调用这个方法。如 当使用“+”连接一个字符串和一个对象或者在希望使用字符串的方法中使用了对象使,都会调用toString
valueOf()方法: JS需要将对象转换为某种原始值而非字符串时才调用它。尤其是转换为数字时,如果在需要使用原始值的上下文中使用了对象,JS会自动调用这个方法。
我重新写了对象的toString, valueOf方法,如果没有,则从原型链往上找,情况如下:
// 对象和字符串2进行比较 var e = {x: '2',valueOf: function() { return '2' }}; console.log( e == '2') //true var e = {x: '2',toString: function() { return '2' }}; console.log( e == '2' ); //true var e = {x: '2',toString: function() { return '2' }, valueOf: function() { return '3' }}; //同时存在toString, valueOf方法 console.log( e == '2' ); //false var e = {x: '2',toString: function() { return '3' }, valueOf: function() { return '2' }}; ///同时存在toString, valueOf方法 console.log( e == '2'); //true //对象和数字2进行比较 var e = {x: 2,valueOf: function() { return 2 }}; console.log( e == 2) //true var e = {x: 2,toString: function() { return 2 }}; console.log( e == 2 ); //true var e = {x: 2,toString: function() { return 2 }, valueOf: function() { return 3 }}; //同时存在toString, valueOf方法 console.log( e == 2 ); //false var e = {x: 2,toString: function() { return 3 }, valueOf: function() { return 2 }}; ///同时存在toString, valueOf方法 console.log( e == 2 ); //true
结论: 逻辑判断不管是和数字还是字符串比较,当实例对象的toString和valueOf同时,只会调用valueOf方法的返回值,若实例对象只有其中一个方法存在,则会默认调用该方法,而不去使用原型链上的方法。因此我个人感觉当对象和一个字符串使用“+”运算符应该是以对象的toString方法为主。
var i = {x:2,toString: function() { return 'hello ' }}; console.log(i + 'world'); //hello world var i = {x:2,valueOf: function() { return 'hello ' }}; console.log(i + 'world'); //hello world var i = {x:2,valueOf: function() { return 'hello ' },toString: function() { return 'HELLO' }}; console.log(i + 'world'); //hello world var i = {x:2,valueOf: function() { return 'HELLO ' },toString: function() { return 'hello' }}; console.log(i + 'world'); // HELLO world
结果出乎我的意料,先看valueOf方法是否存在,然后再看toString方法是否存在。
最后来看一个很有意思的面试题: 如何输出console.log里面的参数?
if ( p== 1 && p == 2 && p ==3 ) { console.log('really?'); } //利用重写valueOf方法 var p = { x: 1, valueOf: function() { return this.x++ } }; // 完美解决
最后
有遇到新的会继续补上。。。