JavaScript 对象
参考资料:《JavaScript高级程序设计》、《JavaScript权威指南第7版》、
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference
一、概述
JavaScript 的设计是一个简单的基于对象的范式。
每个对象都是基于一个引用类型创建的,这个引用类型可以是原生类型,也可以是开发人员定义的类型。
对象是无序属性的集合,其属性可以包含基本值、对象或者函数。
对象最常见的用法是创建、设置、查找、删除、检测和枚举它的属性。
二、原型prototype
每一个JavaScript对象(null除外)都和另一个对象相关联。”另一个“对象就是原型,每个对象都从原型继承属性。
我们创建的每个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个对象,
而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那
么 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以
让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是
可以将这些信息直接添加到原型对象中。
function Person(){ } Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ alert(this.name); }; var person1 = new Person(); person1.sayName(); //"Nicholas" var person2 = new Person(); person2.sayName(); //"Nicholas" alert(person1.sayName == person2.sayName); //true
在此,我们将 sayName() 方法和所有属性直接添加到了 Person 的 prototype 属性中,构造函数
变成了空函数。即使如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属
性和方法。但与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说,
person1 和 person2 访问的都是同一组属性和同一个 sayName() 函数。要理解原型模式的工作原理,
必须先理解 ECMAScript 中原型对象的性质。
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype
属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor
(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。就拿前面的例子来说,
Person.prototype. constructor 指向 Person 。而通过这个构造函数,我们还可继续为原型对象
添加其他属性和方法。
创建了自定义的构造函数之后,其原型对象默认只会取得 constructor 属性;至于其他方法,则
都是从 Object 继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部
属性),指向构造函数的原型对象。ECMA-262 第 5 版中管这个指针叫 [[Prototype]] 。虽然在脚本中
没有标准的方式访问 [[Prototype]] ,但 Firefox、Safari 和 Chrome 在每个对象上都支持一个属性
__proto__ ;而在其他实现中,这个属性对脚本则是完全不可见的。不过,要明确的真正重要的一点就
是,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
以前面使用 Person 构造函数和 Person.prototype 创建实例的代码为例,下图展示了各个对
象之间的关系。
在此, Person.prototype 指向了原型对象,而 Person.prototype.constructor 又指回了 Person 。
原型对象中除了包含 constructor 属性之外,还包括后来添加的其他属性。 Person 的每个实例——
person1 和 person2 都包含一个内部属性,该属性仅仅指向了 Person.prototype ;换句话说,它们
与构造函数没有直接的关系。此外,要格外注意的是,虽然这两个实例都不包含属性和方法,但我们却
可以调用 person1.sayName() 。这是通过查找对象属性的过程来实现的。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先
从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,
则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这
个属性,则返回该属性的值。也就是说,在我们调用 person1.sayName() 的时候,会先后执行两次搜
索。首先,解析器会问:“实例 person1 有 sayName 属性吗?”答:“没有。”然后,它继续搜索,再
问:“ person1 的原型有 sayName 属性吗?”答:“有。”于是,它就读取那个保存在原型对象中的函
数。当我们调用 person2.sayName() 时,将会重现相同的搜索过程,得到相同的结果。而这正是多个
对象实例共享原型所保存的属性和方法的基本原理。
三、创建对象
1)对象字面量
在使用对象字面量语法时,属性名也可以使用字符串。
var person={ name:'dog', age:18 }
//或者
2)new 创建
new 创建并初始化一个新对象,关键字new后跟一个函数调用,这里的函数成为构造函数。
var obj=new Object();//创建一个空对象,和{}一样 obj.name='dog'; var arr=new Array();//创建一个空数组,和[]一样 var date=new Date();//创建一个表示当前时间的日期Date对象 var r=new RegExp('js');//创建一个模式匹配对象
3) Object.create()
所有通过对象字面量创建的对象都具有同一个原型对象,并可以通过Ojbect.prototype获得对原型对象的引用。
通过关键字new和构造函数创建的对象的原型就是构造函数的prototype属性的值。因此使用{}和new Object()创建的对象都继承自Object.prototype.
new Array()创建的对象的原型就是Array.prototype. new Date()创建的对象的原型就是Date.prototype.
Object.prototype没有原型,它不继承任何属性。其他具有原型。所有的内置构造函数,以及大部分自定义的构造函数都具有
一个继承自Object.prototype的原型。例如Date.prototype的属性继承子Object.prototype,因此由new Date()创建的Date对象的属性
同时继承自Date.prototype和Object.prototype。这一系列链接的原型对象就是所谓的”原型链“。
Object.create()是一个静态函数,它创建一个新对象,第一个参数是这个对象的原型。
Object.create()提供第二个可选的参数,用以对对象的属性进一步描述。
语法:Object.create(proto,[propertiesObject])
var person = { name: 'dog', age: 18 } var o1 = Object.create(person); console.log('o1:',o1); console.log('o1.name:',o1.name); console.log('o1.age:',o1.age);
如果想创建一个普通的空对象(例如通过{}或new Object()创建的对象),需要传入Object.prototype。
var o3 = Object.create(Object.prototype);//o3和{}和new Object()一样
继承:下面的例子演示了如何使用Object.create()
来实现类式继承。这是一个所有版本JavaScript都支持的单继承。
// 父类的方法 Shape.prototype.move = function (x, y) { this.x += x; this.y += y; console.info('Shape moved.'); }; // Rectangle - 子类(subclass) function Rectangle() { Shape.call(this); // call super constructor. } // 子类续承父类 Rectangle.prototype = Object.create(Shape.prototype); Rectangle.prototype.constructor = Rectangle; var rect = new Rectangle(); console.log('Is rect an instance of Rectangle?', rect instanceof Rectangle); // true console.log('Is rect an instance of Shape?', rect instanceof Shape); // true rect.move(1, 1); // Outputs, 'Shape moved.' console.log(rect); var shape=new Shape(); console.log(shape);
四、对象属性的查询和设置
可以通过.或者[]来获取对象属性的值,方括号内必须是一个计算结果为字符串的表达式。
var person={ name:'dog', age:18 }; console.log(person.name); console.log(person['age']); console.log(person['a'+'ge']);
属性访问错误处理
let book = { } console.log(book && book.subtitle && book.subtitle.length);//undefined book.subtitle = [1, 2, 3]; console.log(book && book.subtitle && book.subtitle.length);//3
ES6允许用表达式作为属性名,但是一定要将表达式放在方括号内。
let t='dog' var o={ [t+'abc'](){ return '函数执行了'; }, [t+'name']:'wo shi gou ' } console.log(o.dogabc()); console.log(o.dogname);
五、删除属性
delete运算符用来删除对象的属性,delete 只能删除自有属性,不能删除继承属性。
var o={ name:'dog', eat(){ console.log(this.name); } } o.eat();//dog delete o.name; delete o.eat; console.log(o);//{}
六、检测属性
判断某个属性是否存在于某个对象中,可以使用in运算符、hasOwnProperty()和propertyIsEnumerable()方法来完成。
七、枚举属性
可以使用for in ,Object.keys() ,Object.getOwnPropertyNames()
八、存取器属性setter和getter
对象的属性由i名字、值和一组特性构成。在ES5中,属性值可以用一个或两个方法替代,这两个方法就是getter和setter,由其定义的属性称为存取器属性。
var p={ x:1, y:2, get r(){ return this.x+this.y; }, set r(newValue){ this.x=10; this.y=100; }, get theta(){ return 'dog'; } } console.log(p.r);//3 p.r=10; console.log(p.r);//110
九、属性的特性
除了包含名字和值外,属性还包含一些标识他们可写、可枚举和可配置的特性。
可以将对象设置成不可枚举的,这让他们看起来更像内置方法。
可以给对象定义不能修改或删除的属性,借此锁定对象。
将值看成特性,可以认为一个属性包含1个名字和4个特性,4个特性分别是值value、可写性writable、可枚举型enumerable和可配置性configurable。
设置属性的特性,需要调用Object.defineProperty(),传入要修改的对象、要创建或修改的属性的名称以及属性描述符对象。
对于新创建的属性来说,默认的特性值是false或undefined。
var o={}; Object.defineProperty(o,'x',{ value:1, writable:true, enumerable:false, configurable:false }); //属性是存在的,但不可枚举 console.log(o.x);//1 console.log(Object.keys(o));//[] //现在对属性x做修改,让它变为只读 Object.defineProperty(o,'x',{ writable:false }); o.x=2;//操作失败,不报错,严格模式下报错 console.log(o.x);//1 delete o.x; //可配型性控制着对其他特性的修改,包括删除属性。 console.log(o);//o.x没删掉。因为configurable是false
如果要同时修改和创建多个属性,则需要调用Object.defineProperties
var p=Object.defineProperties({},{ x:{ value:1, writable:true, enumerable:true, configurable:true }, r:{ get(){ return this.x+100; }, enumerable:true, configurable:true } }); console.log(p); console.log(p.r); console.log(p.x);
十、其它
1)拓展运算符(...)
扩展运算符用于取出对象所有可遍历属性,然后拷贝到新对象。可用于合并对象。
var p={ name:'dog' } var p2={ age:18 } var o={...p,...p2}; console.log(o);
2)Object.assign
Object.assign(target, source_1, ···)
用于将源对象的所有可枚举属性复制到目标对象中。
assign 的属性拷贝是浅拷贝
var p={ name:'dog' } var p2={ age:18 } var o={ height:176 }; Object.assign(o,p,p2); console.log(o);
3) Object.is()
Object.is()
方法判断两个值是否为同一个值。如果满足以下条件则两个值相等:
- 都是
undefined
- 都是
null
- 都是
true
或false
- 都是相同长度的字符串且相同字符按相同顺序排列
- 都是相同对象(意味着每个对象有同一个引用)
- 都是数字且
- 都是
+0
- 都是
-0
- 都是
NaN
- 或都是非零而且非
NaN
且为同一个值
- 都是
与==
(en-US) 运算不同。 ==
运算符在判断相等前对两边的变量(如果它们不是同一类型) 进行强制转换 (这种行为的结果会将 "" == false
判断为 true
), 而 Object.is
不会强制转换两边的值。
与===
(en-US) 运算也不相同。 ===
运算符 (也包括 ==
运算符) 将数字 -0
和 +0
视为相等 ,而将Number.NaN
与NaN
视为不相等.