阅读 94

JavaScript中的对象学习笔记(属性操作)

本文是笔者学习JavaScript时做的笔记,大部分内容来自《JavaScript权威指南》,记录学习中的重点,并引入一些其他博文和与其他程序员讨论的内容,供本人日常翻阅。如有疑问,请留言评论,对本文的内容想深入了解,请购买正版《JavaScript权威指南》。

一. 属性的查询和设置

  • 对象的属性可以通过(.)或者方括号([])来访问。
  • ES3中,点运算符后面的标识符不能是保留字,比如o.for和o.class是非法的,因为for是js的关键字,class是保留字(ES6中成为关键字)。如果一个对象的属性名是保留字,必须以方括号的形式访问。ES5和ES3的某些实现对其放宽了限制。
  • 严格来讲,方括号内的表达式必须返回字符串或者返回一个可以转换为字符串的值。

二. 继承

js对象具有“自有属性”(own property),也有一些属性是从原型对象继承而来的。当查询某个对象的属性的时候,会沿着原型链向上查找,直到查找到null。

<赋值行为> 对o的属性x进行赋值,如果x中已经有了x属性且不是继承来的,那么这个赋值操作只改变这个已有属性x的值。如果o中不存在属性x,那么赋值操作给o添加一个新的属性x。如果集成有属性x,那么会在对象内重新创建一个同名的对象x。同时,在赋值的过程中,js也会检查将要操作的属性的合法性,如果属性是只读的,那么赋值操作将不被允许。

  • 操作属性的时候,只会在当前对象进行操作,不会修改其原型。
  • 只要在查询属性的时候才会体会到继承的存在,设置属性与继承无关,

三. 属性访问错误

查询一个不存在的属性并不会报错,如果在o的自有属性上或者继承属性上均未查找到x,那么将会返回一个undefined。 但是,如果对象不存在,试图查询这个不存在的对象的属性,就会抛出一个类型错误异常。 为了避免这种查询错误,有几种可行的方法:

	//比较冗余的方法
	let len = undefined;
	if(book){
		if(book.subtitle) len = book.subtitle.length;
	}
	//更简练的方法
	let len = book&&book.subtitle&&book.title.subtitle.length;
复制代码

总结赋值失败的场景:

  1. o中的属性是只读的
  2. o中的属性是继承的,且是只读的。
  3. o中不存在自由属性p,o没有使用setter方法继承属性p,并且o的可扩展性为false。

四. 删除属性

删除属性用的是delete运算符。其作用是,断开当前对象与指定属性的连接,但是这个属性对应的值还当前环境有其它引用时,并不会被销毁。

  • delete运算符只能删除自有属性。
  • 当delete删除成功或没有任何副作用时,返回true
	o = {x:1};
	delete o.x; //删除x返回true
	delete o.x; //无事发生,返回true
	delete o.toStrung; //无事发生,返回true
	delete 1;  //表达式无意义,返回true
复制代码

delete不能删除那些可配置属性为false的属性,但是可以删除不可扩展对象的可配置属性。某些内置对象是不可配置的,比如通过变量声明和函数声明创建的全局对象的属性。严格模式中,删除一个不可配置属性会报类型错误。在费严格模式以及一些ES3的实现中,delete会返回false:

	delete Object.prototype; //属性为不可配置的
	var x = 1;
	delete this.x; //不能删除这个属性
	function f(){}; 
	delete this.f; //不能删除全局函数
复制代码

在非严格模式中删除全局对象的可配置属性时,可以省略对全局对象的引用,直接在delete操作符后跟随要删除的属性名即可。

	this.x = 1;
	delete x;//将他删除
复制代码

在严格模式下,delete后跟随一个非法的操作数,会报一个语法错误,因此必须显式指定对象和其属性。

	delete x;//报类型错误
	delete this.x;//正常工作
复制代码

五. 检测属性

判断某个属性是否存在于某个对象中,是我们常用的操作。

  1. in运算符,左侧为属性名,右侧为检查对象,如果自有属性或者继承属性包含检测的值,则返回true。
  2. 对象的hasOwnProperty()方法用来检测给定的名字是否是对象的自有属性。对于继承属性将返回false。
  3. 对象的propertyIsEnumerable()方法,只有检测这个自有属性是可枚举的时候,才会返回true。
  4. 使用"!==undefined"来判断属性是否存在,但不能区分存在且值为undefined的属性。

六. 枚举属性

利用for/in循环可以遍历目标对象所有的可枚举属性。对象继承的内置方法是不可枚举的,但是在代码中给对象添加的属性都是可枚举的。 许多实用工具库诶Object.prototype添加了新的方法和属性,这些方法可以被所有的对象继承并使用。然而在ES5之前,这些新添的方法不能被定义为不可枚举,因此他们都会被枚举出来。为了避免,需要一些方法跳过这些属性。

	for(p in o){
		if(!o.hasOwnProperty(p)) continue; //跳过继承属性
	}
	for(p in o){
		if(type o[p]==="function") continue; //跳过方法
	}
复制代码

用来枚举属性的工具函数

	/*
	 *枚举p中的属性复制到o中,并返回o
	 *不会处理getter和setter以及复制属性
	 */
	function extend(o, p){
		for(prop in p){
			o[prop] = p[prop];
		}
		return o;
	}
	//本实现并不完全,在ie中会有一些bug,但是在之后会有个更加强大的版本
	
	/*
	 *枚举p中的属性复制到o中,并返回o
	 *不会处理getter和setter以及复制属性
	 *o和p有同名属性,则o的属性将不受影响
	 */
	 function merge(o, p){
		for(prop in p){
			if(o.hasOwnProperty[prop]) continue;
			o[prop] = p[prop];
		}
		return o;
	}
	
	/*
	 *如果o中属性在p中没有同名属性,则从o中删除这个属性
	 */
	 function restrict(o, p){
	 	for(prop in o){
	 		if(!(prop in p)) delete o[prop];
	 	}
	 	return o;
	 }
	 
	 /*
	 *如果o中属性在p中有同名属性,则从o中删除这个属性
	 */
	 function substract(o,p){
	 	for(prop in p){
	 		delete o[prop];
	 	}      
	 	return o;
	 }
	 
	 /*
	  *返回一个对象,这个对象同时有o和p的属性
	  *如果o和p有重名的属性,使用p的属性
	  */
	  function union(o, p){ return extend(extend({},o),p) };
	  
	  /*
	  *返回一个对象,这个对象同时有在o和p中出现的属性
	  *如果o和p有重名的属性,使用p的属性
	  */
	  function intersection(o, p){ return restrict(extend({},o),p)}
	  /*
	   *返回一个数组,这个数组包含的是o中可枚举的属性的名字
	   */
	  function keys(o){
	  	if(typeof o!== "object") throw TypeError();
	  	var result = [];
	  	for(var prop in o){
	  		if(o.hasOwnProperty(prop));
	  		result.push(prop);
	  	}
	  	return result;
	  }
复制代码
  • 除了for/in循环,ES5也定义了两个用来枚举属性名称的函数,第一个是Object.keys(),它返回一个数组,这个数组由对象中可枚举的自有属性的名称组成。
  • 第二个函数是,Object.hasOwnPropertyNames(),它和Object.keys()类似,只是它返回的是对象所有自有属性的名字,而不仅仅是可枚举属性,ES3则无法模拟,因为ES3没有提供任何获取对象不可枚举属性的方法。

七. 属性getter和setter

js对象中,属性是由名字、值和一组特性构成的。在ES5和除了ie之外的较新的ES3实现,属性值可以由一个或两个方法代替,这两个方法就是getter和setter。由getter和setter定义的属性被称为“存取器属性”(accessor property),不同于“数据属性”(data property),数据属性只有一个简单的值。

  • 程序访问存取器属性时,js调用getter方法。这个方法的返回值就是属性存取表达式的值。
  • 当程序设置一个存取器属性的值时,js调用setter方法,将赋值表达式右侧的值当做参数传入setter。(可以忽略该方法的返回值)
	var o = {
		data_prop:"value",
		get accessor_prop(){/*这里是函数体*/},
		set accessor_prop(){/*这里是函数体*/}
	}
复制代码

方法 存取器属性定义为一个或两个和属性同名的函数,使用get和set关键字。以下为一个笛卡尔坐标的对象。

	var p = {
		x:1.0,
		y:1.0,
		get r() { return Math.sqrt(this.x + this.y*this.x)},
		set r(newvalue){
			var oldvalue  = Math.sqrt(this.x*this.x + this.y*this.y);
			var ratio = newvalue/oldvalue;
			this.x*=ratio;
			this.y*=ratio;
		},
		get theta(){ return Math.atan2(this.y,this.x) }
	}
复制代码

需要注意getter和setter里this关键词的用法,js把这些函数作为当前对象的方法来调用,也就是说是,在函数体内this指向这个点的对象。

和数据的属性一样,存取器属性是可以继承的,因此可以将对象p当做一个点的原型。

	var q = inherit(p);
	q.x=1;
	q.y=1;
	console.log(q.r);
	console.log(q.theta);
复制代码

这段代码使用存取器属性定义api,api提供了表示同一组数据的两种方法(笛卡尔坐标系表示法和极坐标系表示法)。再来一个例子:

	//这个对象产生严格自增的序列号
	var serialnum = {
		$n: 0,
		get next(){ return this.$n++ },
		set next(){ 
			if(n >= this.$n) this.$n = n;
			else throw "序列号的值不能比当前值小";
		}
	}
复制代码

八. 属性的特性

属性除了包含名字和值之外,属性还包含一些标识他们可写,可枚举和可配置的特性。在ES3中无法设置这些特性,所有通过ES3的程序创建的属性都是可写的、可枚举的和可配置的,且无法对这些特性修改。ES5中定义了查询和设置这些特性的API。这些API对于库的开发和来说很重要,因为:

  • 可以通过这些API给原型对象添加方法,并将它们设置为不可枚举的,让它们看起来更像内置方法。
  • 可以通过这些API给对象定义不能修改或删除的属性,借此“锁定”这个对象。

在本节,我们将存取器的getter和setter也看成是属性的特性,那么照这个逻辑,我们也可以把数据属性的值,同样看成一个属性名和4个特性:值(value)、可写性(writable)、可枚举性(enumerable)、可配置型(configurable)。 存取器属性不具有值特性和可写性,他们的可写性是由setter方法存在与否决定的。因此存取器属性的4个特性是,读取(get)、写入(set)、可枚举性和可配置性。 为了实现属性特性的查询和设置操作,ES5中定义了一个名为“属性描述符”(property descriptor)的对象,这个对象代表四个特性,且描述符的属性和它们所描述的属性特性是同名的。因此,描述符对象的属性有,value、writable、enumerable和configurable。

  • 通过Object.getOwnPropertyDescriptor()可以获取某个对象特定属性的属性描述符。
  • 设置属性的特性,或者想让新建的属性具有某种特性,则需要Object.definePeoperty(),传入要修改的对象,或修改的属性的名称以及属性描述符对象。
	var o = {};//创建一个空对象
	//添加一个不可枚举的属性x,并赋值为1
	Object.defineProperty(o, "x", {
		value:1,
		writable:true,
		enumerable:false,
		configurable:false
	});
	
	//属性是存在的,但不可枚举
	o.x;	//=>1
	Object.keys(o) //=>[]
	
	//现在对x进行修改,让它变为只读
	Object.defineProperty(o,"x",{writable:false});
	
	//现在修改这个属性,修改失败,但不会报错,ES5严格模式下会报错
	o.x = 2;
复制代码

由上例子所示,Object.defineProperty()的属性描述对象不必包含全部四个属性(不能用来修改继承属性) 还有一个进阶版的方法,Object.defineProperties(),第一个参数是要修改的对象,第二个是一个映射表。

规则

  • obj不可扩展,则可以编辑已有的自有属性,但不能添加新属性。
  • 如果属性是不可配置的,则不能修改它的可配置性和可枚举性。
  • 如果存取器属性是不可配置的,则不能修改getter和setter方法,也不能将它转为数据属性。
  • 如果数据属性是不可配置的,则不能将它转换为存取器属性。
  • 如果数据属性是不可配置的,则不能将它的可行写从false修改为true,但可以从true置为false。
  • 如果数据属性是不可配置且不可写的,则不能修改它的值。然而可配置但不可写属性的值是可以修改的。
	/*
	 *给Object.prototype添加一个不可枚举的extend()方法
	 *这个方法继承自调用它的对象,将作为参数传入的对象的属性一一复制
	 *除了值之外,也复制属性的所有特性,除非在目标对象中存在同名的属性。
	 */
	 Object.defineProperty(Object.prototype,
		 "extend",	//定义Object.propertype.extend
		 {
		 	writable: true,
		 	enumerable: false,
		 	configurable: true,
		 	value: function(o){
		 		writable: true
		 		var names = Object.getOwnPropertyNmaes(o);
		 		for(var i=0;i < names.length;i++){
		 			//如果属性已存在,则跳过
		 			if(names[i] in this) continue;
		 			var desc = Object.getOwnPropertyDescriptor(o, names[i]);
		 			//用它给this创建一个属性
		 			Object.defineProperty(this,names[i],desc);
		 		}
		 	}
		 }
	 )
复制代码
文章分类
前端