js对象实例详解(JavaScript对象深度剖析,深度理解js对
象)
这算是酝酿很久的⼀篇⽂章了。
JavaScript作为⼀个基于对象(没有类的概念)的语⾔,从⼊门到精通到放弃⼀直会被对象这个问题围绕。
平时发的⽂章基本都是开发中遇到的问题和对最佳解决⽅案的探讨,终于忍不住要写⼀篇基础概念类的⽂章了。
本⽂探讨以下问题,在座的朋友各取所需,欢迎批评指正:
1、创建对象
2、__proto__与prototype
3、继承与原型链
4、对象的深度克隆
5、⼀些Object的⽅法与需要注意的点
6、ES6新增特性
下⾯反复提到实例对象和原型对象,通过构造函数 new 出来的本⽂称作实例对象,构造函数的原型属性本⽂称作原型对象。
创建对象
字⾯量的⽅式:
var myHonda = {color: "red", wheels: 4, engine: {cylinders: 4, size: 2.2}}
就是new Object()的语法糖,⼀样⼀样的。
⼯⼚模式:
function createCar(){
var oTemp = new Object();
oTemp.name = arguments[0];
//直接给对象添加属性,每个对象都有直接的属性
oTemp.age = arguments[1];
oTemp.showName = function () {
alert(this.name);
};//每个对象都有⼀个 showName ⽅法版本
return oTemp;
};
var myHonda = createCar('honda', 5)
只是给new Object()包了层⽪,⽅便量产,并没有本质区别,姑且算作创建对象的⼀种⽅式。
构造函数:
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
return this.name;
};
}
var rand = new Person("Rand McKinnon", 33, "M");
上⾯构造函数的 getName ⽅法,每次实例化都会新建该函数对象,还形成了在当前情况下并没有卵⽤的闭包,所以构造函数添加⽅法⽤下⾯⽅式处理,⼯⼚模式给对象添加⽅法的时候也应该⽤下⾯的⽅式避免重复构造函数对象
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
function getName() {
return this.name;
};
构造函数创建对象的过程和⼯⼚模式⼜是半⽄⼋两,相当于隐藏了创建新对象和返回该对象这两步,构造函数内 this 指向新建对象,没什么不同。
最⼤不同点: 构造函数创造出来的对象 constructor 属性指向该构造函数,⼯⼚模式指向 function Object(){...}。
构造函数相当于给原型链上加了⼀环,构造函数有⾃⼰的 prototype,⼯⼚模式就是个普通函数。说到这
⼉我上⼀句话出现了漏洞,⼯⼚模式的 constructor 指向哪得看第⼀句话 new 的是什么。
构造函数直接调⽤⽽不 new 的话,就看调⽤时候 this 指向谁了,直接调⽤就把属性绑到 window 上了,通过 call 或者 apply 绑定到其他对象作⽤域就把属性添加到该对象了。
原型模式:
构造函数虽然在原型链上加了⼀环,但显然这⼀环啥都没有啊,这样⼀来和⼯⼚模式⼜有什么区别?加了⼀环⼜有什么意义?原型模式浮出⽔⾯。
function Car(){}
//⽤空构造函数设置类名
lor = "blue";//每个对象都共享相同属性
Car.prototype.doors = 3;
Car.prototype.drivers = new Array("Mike","John");
Car.prototype.showColor = function(){
lor);
};//每个对象共享⼀个⽅法版本,省内存。
//构造函数的原型属性可以通过字⾯量来设置,别忘了通过 Object.defineProperty()设置 constructor 为该构造函数
function Car(){}
Car.prototype = {
color:"blue",
doors:3,
showColor:function(){
lor);
}
}
Object.defineProperty(Car.prototype, "constructor", { enumerable:false, value:Car })
//(不设置 constructor 会导致 constructor 不指向构造函数,直接设置 constructor 会导致 constructor 可枚举)
使⽤原型模式注意动态性,通过构造函数实例化出的对象,他的原型对象是构造函数的 prototype ,如果在他的原型对象上增加或删除⼀些⽅法,该对象会继承这些修改。例如,先通过构造函数 A 实例化出对象 a ,然后再给 A.prototype 添加⼀个⽅法,a 是可以继承这个⽅法的。但是给 A.prototype 设置⼀个新的对象,a 是不会继承这个新对象的属性和⽅法的。听起来有点绕,修改 A.prototype 相当于直接修改 a 的原型对象,a 很⾃然的会继承这些修改,但是重新给 A.prototype 赋值的话,修改的是构造函数的原型,并没有影响 a 的原型对象!a 被创建出来以后原型对象就已经确定了,除⾮直接修改这个原型对象(或者这个原型对象的原型对象),否则 a 是不会继承这些修改的!
传⼊要创建对象实例的原型对象,和原型模式⼏乎是⼀个意思也是相当于在原型链上加了⼀环,区别在于这种⽅式创建的对象没有构造函数。这种⽅式相当于:
function object(o){
function F(){}
F.prototype = o;
return new F()
}
相当于构造函数只短暂的存在了⼀会,创建出来的对象的 constructor 指向原型对象 o 的 constructor !
混合模式:
使⽤原型模式时,当给实例对象设置⾃⼰专属的属性的时候,该实例对象会忽略原型链中的该属性。但当原型链中的属性是引⽤类型值的时候,操作不当有可能会直接修改原型对象的属性!这会影响到所有使⽤该原型对象的实例对象!
⼤部分情况下,实例对象的多数⽅法是共有的,多数属性是私有的,所以属性在构造函数中设置,⽅法在原型中设置是合适的,构造函数与原型结合使⽤是通常的做法。
javascript数组对象
还有⼀些⽅法,⽆⾮是⼯⼚模式与构造函数与原型模式的互相结合,在⽣成过程和 this 指向上做⼀些⼩变化。
class ⽅式:
见下⾯ ES6 class 部分,只是⼀个语法糖,本质上和构造函数并没有什么区别,但是继承的⽅式有⼀些区别。
proto与prototype
这两个到底是什么关系?搞清楚实例对象构造函数原型对象的三⾓关系,这两个属性的⽤法就⾃然清晰了,顺便说下constructor。
构造函数创建的实例对象的 constructor 指向该构造函数(但实际上 constructor 是对应的原型对象上的⼀个属性!所以实例对
象的 constructor 是继承来的,这⼀点要注意,如果利⽤原型链继承,constructor 将有可能指向原型对象的构造函数甚⾄更上层的构造函数,其他重写构造函数 prototype 的⾏为也会造成 constructor 指向问题,都需要重设 constructor),构造函数的prototype 指向对应的原型对象,实例对象的 __proto__ 指对应的原型对象,__proto__是浏览器的实现,并没有出现在标准中,可以⽤ constructor.prototype 代替。考虑到 ate() 创建的对象,更安全的⽅法是 PrototpyeOf() 传⼊需要获取原型对象的实例对象。
我⾃⼰都感觉说的有点乱,但是他们就是这样的,上⼀张图,看看能不能帮你更深刻理解这三者关系。
继承与原型链
当访问⼀个对象的属性时,如果在对象本⾝不到,就会去搜索对象的原型,原型的原型,知道原型链的尽头 null,那原型链是怎么链起来的?
把实例对象构造函数原型对象视为⼀个⼩组,上⾯说了三者互相之间的关系,构造函数是函数,可实例对象和原型对象可都是普通对象啊,这就出现了这样的情况:
这个⼩组的原型对象,等于另⼀个⼩组实例对象,⽽此⼩组的原型对象⼜可能是其他⼩组的实例对象,这样⼀个个的⼩组不就连接起来了么。举个例⼦:
function Super(){
this.val = 1;
this.arr = [1];
}
function Sub(){
/
/ ...
}
Sub.prototype = new Super();
Sub 是⼀个⼩组 Super 是⼀个⼩组,Sub 的原型对象链接到了 Super 的实例对象。
基本上所有对象顺着原型链爬到头都是 Object.prototype , ⽽ Object.prototype 就没有原型对象,原型链就⾛到头了。
判断构造函数和原型对象是否存在于实例对象的原型链中:
实例对象 instanceof 构造函数,返回⼀个布尔值,原型对象.isPrototypeOf(实例对象),返回⼀个布尔值。
上⾯是最简单的继承⽅式了,但是有两个致命缺点:
所有 Sub 的实例对象都继承⾃同⼀个 Super 的实例对象,我想传参数到 Super 怎么办?
如果 Super ⾥有引⽤类型的值,⽐如上⾯例⼦中我给 Sub 的实例对象中的 arr 属性 push ⼀个值,岂不
是牵⼀发动全⾝?
下⾯说⼀种最常⽤的组合继承模式,先举个例⼦:
function Super(value){
// 只在此处声明基本属性和引⽤属性
this.val = value;
this.arr = [1];
}
// 在此处声明函数
Super.prototype.fun1 = function(){};
Super.prototype.fun2 = function(){};
//Super.
function Sub(value){
Super.call(this,value); // 核⼼
// ...
}
Sub.prototype = new Super(); // 核⼼
过程是这样的,在简单的原型链继承的基础上, Sub 的构造函数⾥运⾏ Super ,从⽽给 Sub 的每⼀个实例对象⼀份单独的属性,解决了上⾯两个问题,可以给 Super 传参数了,⽽且因为是独⽴的属性,不会因为误操作引⽤类型值⽽影响其他实例了。不过还有个⼩缺点: Sub 中调⽤的 Super 给每个 Sub 的实例对象⼀套新的属性,覆盖了继承的 Super 实例对象的属性,那被覆盖的的那套属性不就浪费了?岂不是⽩继承了?最严重的问题是 Super 被执⾏了两次,这不能忍(其实也没多⼤问题)。下⾯进⾏⼀下优化,把上⾯例⼦最后⼀⾏替换为:
Sub.prototype = ate(Super.prototype);
// ate() 给原型链上添加⼀环,否则 Sub 和 Super 的原型就重叠了。
structor = Sub;
到此为⽌,继承⾮常完美。
其他还有各路继承⽅式⽆⾮是在简单原型链继承 --> 优化的组合继承路程之间的⼀些思路或者封装。
通过 class 继承的⽅式:
通过 class 实现继承的过程与 ES5 完全相反,详细见下⾯ ES6 class的继承部分。
对象的深度克隆
JavaScript的基础类型是值传递,⽽对象是引⽤传递,这导致⼀个问题:
克隆⼀个基础类型的变量的时候,克隆出来的的变量是和旧的变量完全独⽴的,只是值相同⽽已。
⽽克隆对象的时候就要分两种情况了,简单的赋值会让两个变量指向同⼀块内存,两者代表同⼀个对象,甚⾄算不上克隆克隆。但我们常常需要的是两个属性和⽅法完全相同但却完全独⽴的对象,称为深度克隆。我们接下来讨论⼏种深度克隆的⽅法。
说⼏句题外的话,业界有⼀个⾮常知名的库 immutable ,个⼈认为很⼤程度上解决了深度克隆的痛点,
我们修改⼀个对象的时候,很多时候希望得到⼀个全新的对象(⽐如Redux每次都要⽤⼀个全新的对象修改状态),由此我们就需要进⾏深度克隆。⽽ immutable 相当于产⽣了⼀种新的对象类型,每⼀次修改属性都会返回⼀个全新的 immutable 对象,免去了我们深度克隆的⼯作是⼩事,关键性能特别好。
历遍属性
function clone(obj){
var newobj = structor === Array ? [] : {}; // ⽤ instanceof 判断也可
if(typeof obj !== 'object' || obj === null ){
return obj
} else {
for(var i in obj){
newobj[i] = typeof obj[i] === 'object' ? cloneObj(obj[i]) : obj[i];
// 只考虑对象和数组,函数虽然也是引⽤类型,但直接赋值并不会产⽣什么副作⽤,所以函数类型⽆需
深度克隆。
}
}
return newobj;
};
原型式克隆
function clone(obj){
function F() {};
F.prototype = obj;
var f = new F();
for(var key in obj)
{
if(typeof obj[key] =="object")
{
f[key] = clone(obj[key])
}
}
return f ;
}
这种⽅式不能算严格意义上的深度克隆,并没有切断新对象与被克隆对象的联系,被克隆对象作为新对象的原型存在,虽然新对象的改变不会影响旧对象,但反之则不然!⽽且给新对象属性重新赋值的时候只是覆盖了原型中的属性,在历遍新对象的时候也会出现问题。这种⽅式问题重重,除了实现特殊⽬的可以酌情使⽤,通常情况应避免使⽤。
json序列化
var newObj = JSON.parse(JSON.stringify(obj));
这是我最喜欢的⽅式了!简短粗暴直接!但是最⼤的问题是,毕竟JSON只是⼀种数据格式所以这种⽅式只能克隆属性,不能克隆⽅法,⽅法在序列化以后就消失了。。。
⼀些Object的⽅法与需要注意的点
Object ⾃⾝的⽅法:
设置属性,Object.defineProperty(obj, prop, descriptor) 根据 descriptor 定义 obj 的 prop 属性(值,是否可写可枚举可删除等)。
使对象不可拓展,Object.preventExtensions(obj),obj 将不能添加新的属性。
判断对像是否可拓展,Object.isExtensible(obj)。
密封⼀个对象,Object.seal(obj),obj 将不可拓展且不能删除已有属性。
判断对象是否密封,Object.isSealed(obj)。
冻结对象,Object.freeze(obj) obj 将被密封且不可修改。
判断对象是否冻结,Object.isFrozen(obj)。
获取对象⾃⾝属性(包括不可枚举的),OwnPropertyNames(obj),返回 obj 所有⾃⾝属性组成的数组。
获取对象⾃⾝属性(不包括不可枚举的),Object.keys(obj),返回 obj 所有⾃⾝可枚举属性组成的数组。
当使⽤for in循环遍历对象的属性时,原型链上的所有可枚举属性都将被访问。
只关⼼对象本⾝时⽤Object.keys(obj)代替 for in,避免历遍原型链上的属性。
获取某对象的原型对象,PrototypeOf(object),返回 object 的原型对象。
设置某对象的原型对象,Object.setPrototypeOf(obj, prototype),ES6 新⽅法,设置 obj 的原型对象为 prototype ,该语句⽐较耗时。
Object.prototype 上的⽅法:
检查对象上某个属性是否存在时(存在于本⾝⽽不是原型链中),obj.hasOwnProperty() 是唯⼀可⽤的⽅法,他不会向上查原型链,只在 obj ⾃⾝查,返回布尔值。
检测某对象是否存在于参数对象的原型链中,obj.isPrototypeOf(obj2),obj 是否在 obj2 的原型链中,返回布尔值。
检测某属性是否是对象⾃⾝的可枚举属性,obj.propertyIsEnumerable(prop),返回布尔值。
对象类型,String(),返回 "[object type]" type 可以是 Date,Array,Math 等对象类型。
obj.valueOf(),修改对象返回值时的⾏为,使⽤如下:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。