Javascript中的对象

(一)对象

对象可以通过两种形式定义:声明(文字)形式和构造形式。
对象的文字语法大概是这样:

var myObj = {
	key: value
	// ...
};

构造形式:

var myObj = new Object();
myObj.key = value;
//...

构造形式和文字形式生成的对象是一样的。唯一的区别是,在文字声明中你可以添加多个
键 / 值对,但是在构造形式中你必须逐个添加属性。

1.1 属性访问与键访问

var myObject = {
	a: 2
};
myObject.a; // 2 属性访问
myObject["a"]; // 2 若是需要计算的/可变的/含有特殊字符的键值,则必须使用这种方式访问

1.2 javascript中的数据类型

string、number、boolean、null、undefined、object、function(object的子类型)

1.3 javascript中的内置对象

String、Number、Boolean、Object、Function、Array、Date、RegExp、Error

1.4 typeof 与 instanceof 与 isPrototypeOf

//typeof返回值是一个字符串,该字符串说明运算数的类型。
//typeof 一般只能返回如下几个结果:"number"、"string"、"boolean"、"object"、"function" 和 "undefined"。
function func(){}
/**特殊的object返回值**/
//在 JavaScript 中二进制前三位都为 0 的话会被判断为 object 类型,
//null 的二进制表示是全 0,自然前三位也是 0,所以执行 typeof 时会返回“object”。
console.log(typeof (null));//object
var a = null;
console.log((!a && typeof a === "object")); // true 判断null的方式
//对比
console.log(typeof (123));//typeof(123)返回number
console.log(typeof ("123"));//typeof("123")返回string
console.log(typeof (new String("123")));//object
console.log(typeof (new Number(123)));//object
console.log(new String("123") instanceof String);//true
console.log(new Number(123) instanceof Number);//true

console.log(typeof (undefined));//undefined
console.log(typeof (func));//function
console.log(typeof(new Object())); //object
/*typeof对于丰富的对象实例,只能返回"Object"字符串。
instanceof用来判断对象,代码形式为obj1 instanceof obj2,obj2必须为对象,否则会报错!其返回值为布尔值。
A.isPrototypeOf(B) 判断的是A原型链是否存在于B对象的原型链之中
A instanceof B  判断的是B.prototype是否存在与A的原型链之中*/
function Aoo(){}
function Foo(){} 
Foo.prototype = new Aoo();//JavaScript 原型继承 
var foo = new Foo(); 
console.log(foo instanceof Foo)//true 
console.log(foo instanceof Aoo)//true
console.log(Object.getPrototypeOf( foo ) === Foo.prototype);//true
console.log(Aoo.prototype.isPrototypeOf( Foo.prototype ));//true
/****总结:A原型链存在于B原型链中,则A原型链是B原型链的上级原型链****/

console.log(Object instanceof Object);//true 
console.log(Function instanceof Function);//true 
console.log(123 instanceof Number);//false
console.log("123" instanceof String);//false 
console.log("123".length);//3 会自动转换成包装类型
console.log(Function instanceof Object);//true  
console.log(Foo instanceof Function);//true 
console.log(Foo instanceof Foo);//false

1.5 对象的复制

由于 Object.assign(…) 就是使用 = 操作符来赋值,所
以源对象属性的一些特性(比如 writable)不会被复制到目标对象。

var newObj = Object.assign( {}, myObject );
newObj.a; // 2
newObj.b === anotherObject; // true
newObj.c === anotherArray; // true
newObj.d === anotherFunction; // true

1.6 数组

数组也支持 [] 访问形式,不过就像我们之前提到过的,数组有一套更加结构化的值存储机制(不过仍然不限制值的类型)。数组期望的是数值下标,也就是说值存储的位置(通常被称为索引)是整数。

//数组也是对象,所以虽然每个下标都是整数,你仍然可以给数组添加属性:
//但不建议这么做,这么做也不会修改数组的长度
var myArray = [ "foo", 42, "bar" ];
myArray.baz = "baz";
myArray.length; // 3 长度未改变
myArray.baz; // "baz"
//如果你试图向数组添加一个属性,但是属性名可转换成数字,
//那它会变成一个数值下标(因此会修改数组的内容而不是添加一个属性)
var myArray = [ "foo", 42, "bar" ];
myArray["3"] = "baz"; //"3"会自动转换成3
myArray.length; // 4 长度+1
myArray[3]; // "baz"

1.7 属性描述符

/**验证writable**/
var myObject = {};
Object.defineProperty( myObject, "a", {
	value: 2,
	writable: false, //设置为不可编辑 值
	configurable: true,
	enumerable: true//如果把 enumerable 设置成 false,这个属性就不会出现在枚举(例如for..in)中,
					//虽然仍然可以正常访问它
} );
myObject.a = 3;//修改值无效
console.log(myObject.a); // 2 严格模式下,TypeError
/**验证configurable**/
Object.defineProperty( myObject, "a", {
	value: 4,
	writable: true,
	configurable: false, // 设置为不可配置!
	enumerable: true
} );
console.log(myObject.a); // 4
myObject.a = 5;
console.log(myObject.a); // 5
//因为设置了不可配置,不可删除
delete myObject.a;
console.log(myObject.a); //5 删除失败
//因为设置了不可配置,故TypeError,不可配置
Object.defineProperty( myObject, "a", {
	value: 6,
	writable: true,
	configurable: true,
	enumerable: true
} ); // TypeError

1.7.1 对象不变性

/**1. 对象常量**/
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
	value: 1,
	writable: false,
	configurable: false
} );
myObject.b = 1;
console.log(myObject.b);//1 此时不可配置,不能修改已有属性,但仍然能添加属性

/**2. 禁止扩展**/
var myObject = {
	a:2
};
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; // undefined 此时可配置,可修改已有属性,但无法扩展属性

/**3. 密封**/
//Object.seal(..) 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用
//Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false。

/**4. 冻结**/
//Object.freeze(..) 会创建一个冻结对象,这个方法实际上会在一个现有对象上调用
//Object.seal(..) 并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们的值。
//这个方法是你可以应用在对象上的级别最高的不可变性。

1.8 Getter/Setter

/**Getter的两种声明方式**/
var myObject = {
	// 给 a 定义一个 getter
	get a() {
		return 2;
	}
};
Object.defineProperty(myObject, // 目标对象
	"b", // 属性名
	{ // 描述符
		// 给 b 设置一个 getter
		get: function(){ 
			return this.a * 2 
		},
		// 确保 b 会出现在对象的属性列表中
		enumerable: true
	}
);
myObject.a = 3;//由于没有定义a的setter,故会忽略赋值操作,即使定义了setter也没法改变getter(返回固定值2);
myObject.a; // 2
console.log(myObject.a); // 2 
console.log(myObject.b); // 4

/**Setter**/
//通常来说 getter 和 setter 是成对出现的(只定义一个的话通常会产生意料之外的行为)
var myObject = {
	// 给 a 定义一个 getter
	get a() {
		return this._a_;
	},
	// 给 a 定义一个 setter
	set a(val) {
		this._a_ = val * 2;
	}
};
myObject.a = 2;
myObject.a; // 4

1.8.1 存在性

不可枚举,for…in,Object.keys中都无法查询到。

var myObject = { };
Object.defineProperty(
	myObject,
	"a",
	// 让 a 像普通属性一样可以枚举
	{ enumerable: true, value: 2 }
);
Object.defineProperty(
	myObject,
	"b",
	// 让 b 不可枚举
	{ enumerable: false, value: 3 }
);
myObject.b; // 3 仍然能访问值
("b" in myObject); // true
myObject.hasOwnProperty( "b" ); 
// true hasOwnProperty(..) 只会检查属性是否在 myObject 对象中,不会检查 [[Prototype]] 链。

myObject.propertyIsEnumerable( "a" ); // true
myObject.propertyIsEnumerable( "b" ); // false
for (var k in myObject) {
	console.log( k, myObject[k] );
}// "a" 2 
Object.keys( myObject ); // ["a"]
Object.getOwnPropertyNames( myObject ); // ["a", "b"]

1.9 遍历

//数组有内置的 @@iterator,因此 for..of 可以直接应用在数组上。
var myArray = [ 1, 2, 3 ];
for(var num of myArray){
	console.log("array:"+num);
}//array:1//array:2//array:3
var it = myArray[Symbol.iterator]();
it.next(); // { value:1, done:false } 
it.next(); // { value:2, done:false } 
it.next(); // { value:3, done:false } 
it.next(); // { done:true }

//可以给任何想遍历的对象定义 @@iterator,举例来说:
var myObject = {
	a: 2,
	b: 3
};
//此处代码会报错
//Uncaught TypeError: myObject is not iterable
for(var num of myObject){
	console.log("object:"+num);
}

Object.defineProperty( myObject, Symbol.iterator, {
	enumerable: false,
	writable: false,
	configurable: true,
	value: function() {
		var o = this;
		var idx = 0;
		var ks = Object.keys( o );
		return {
			next: function() {
				return {
					value: o[ks[idx++]],
					done: (idx > ks.length)
				};
			}
		};
	}
});
//定义完Symbol.iterator,即可手动遍历对象 myObject
var it = myObject[Symbol.iterator]();
it.next(); // { value:2, done:false } 
it.next(); // { value:3, done:false } 
it.next(); // { value:undefined, done:true }
// 用 for..of 遍历 myObject
for(var num of myObject){//Uncaught TypeError: myObject is not iterable
	console.log("object:"+num);
}
//object:2//object:3

(二)原型

每个实例对象( object )都有一个私有属性(称之为 _proto_ )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( _proto_ ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。
对于默认的 [[Get]] 操作来说,如果无法在对象本身找到需要的属性,就会继续访问对象
的 [[Prototype]] 链;

2.1 属性屏蔽

下面我们分析一下如果 a不直接存在于 myObject 中而是存
在于原型链上层时 myObject.a= 3 会出现的三种情况。

  1. 如果在 [[Prototype]] 链上层存在名为 a 的普通数据访问属性并且没
    有被标记为只读(writable:false),那就会直接在 myObject 中添加一个名为 a 的新
    属性,它是屏蔽属性(屏蔽了原型链中的a)。
  2. 如果在 [[Prototype]] 链上层存在 a,但是它被标记为只读(writable:false),那么
    无法修改已有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下,代码会
    抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
  3. 如果在 [[Prototype]] 链上层存在 c 并且它是一个 setter,那就一定会
    调用这个 setter。c 不会被添加到(或者说屏蔽于)myObject,也不会重新定义 c 这
    个 setter。
var anotherObject = {
	a:2,
	b:2,
	c:2
};
Object.defineProperty(anotherObject, "c", { 
		set: function(val){ 
			console.log("setter be used!"); 
		},
		get:function(){
			return 2;
		}
	}
);
// 创建一个关联到 anotherObject 的对象
var myObject = Object.create( anotherObject );
myObject.a; // 2 位于myObject的原型链中
anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false hasOwnProperty不会去查找原型链

myObject.a = 3;
console.log(myObject.a);//3 发生了屏蔽,prototype底层的a把上层的a屏蔽了

myObject.b++; // 隐式屏蔽!<==>myObject.b = _proto_.b + 1;
anotherObject.b; // 2
myObject.b; // 3
myObject.hasOwnProperty( "b" ); // true 由于新建了b属性

myObject.c = 3;//setter be used! 调用了原型链上层的c的setter,并未在myObject新增c属性
console.log(myObject.c);//2 依然是取的原型链中的c

2.2 构造函数调用

NothingSpecial 只是一个普通的函数,但是使用 new 调用时,它就会构造一个对象并赋值给 a,这看起来像是 new 的一个副作用(无论如何都会构造一个对象)。这个调用是一个构造函数调用,但是 NothingSpecial 本身并不是一个构造函数。
换句话说,在 JavaScript 中对于“构造函数”最准确的解释是,所有带 new 的函数调用。
函数不是构造函数,但是当且仅当使用 new 时,函数调用会变成“构造函数调用”。

function NothingSpecial() {
	console.log( "Don't mind me!" );
}
var a = new NothingSpecial();// "Don't mind me!" 
console.log(a); // {}

2.3 原型继承

下面这段代码使用的就是典型的“原型风格”:

function Foo(name) {
	this.name = name;
}
Foo.prototype.myName = function() {
	return this.name;
};
function Bar(name,label) {
	Foo.call( this, name );
	this.label = label;
}
// 我们创建了一个新的 Bar.prototype 对象并关联到 Foo.prototype
Bar.prototype = Object.create( Foo.prototype );//这叫原型继承
/**注意!原先有(原先会通过原型链找到 Object的prototype),但是修改完,现在没有 Bar.prototype.constructor 了,Object.prototype中有,如果你需要这个属性的话可能需要手动修复一下它**/ 
//like this
// 需要在 Foo.prototype 上“修复”丢失的 .constructor 属性
// 新对象属性起到 Foo.prototype 的作用
// 关于 defineProperty(..),参见第 3 章
/*Object.defineProperty( Foo.prototype, "constructor" , {
	enumerable: false,
	writable: true,
	configurable: true,
	value: Foo // 让 .constructor 指向 Foo
} );*/
Bar.prototype.myLabel = function() {
	return this.label;
};
var a = new Bar( "a", "obj a" );//此处为实例化
a.myName(); // "a"
a.myLabel(); // "obj a"

解析:
上段代码的核心部分就是语句 Bar.prototype = Object.create( Foo.prototype )。调用
Object.create(…) 会凭空创建一个“新”对象并把新对象内部的 [[Prototype]] 关联到你
指定的对象(本例中是 Foo.prototype)。

// 和你想要的机制不一样!
Bar.prototype = Foo.prototype;
/**Bar.prototype = Foo.prototype 并不会创建一个关联到 Bar.prototype 的新对象,
它只是让 Bar.prototype 直接引用 Foo.prototype 对象。你在Bar.prototype做的修改会直接修改原对象。**/

// 基本上满足你的需求,但是可能会产生一些副作用
Bar.prototype = new Foo();
/**Bar.prototype = new Foo() 的确会创建一个关联到 Bar.prototype 的新对象。
但是它使用了 Foo(..) 的“构造函数调用”,如果函数 Foo 有一些副作用(比如写日志、修改状态、
注册到其他对象、给 this 添加数据属性,等等)的话,就会影响到 Bar() 的“后代”,后果不堪设想。**/

(三)行为委托

3.1 "面向对象"风格的代码:(不推荐)

//与 ·"对象关联"风格的代码:(推荐) 的第一案例对应
function Task(id){
	this.id = id;
}
Task.prototype.outputID = function(){
	console.log(this.id);
}
function XYZ(id, label){
	this.id = id;
	this.label= label;
}
// 让 XYZ“继承”Task
XYZ.prototype = Object.create( Task.prototype );
XYZ.prototype.outputTaskDetails = function (){
	this.outputID ();
	console.log(this.label);
}
var a = new XYZ("work", "7 hours");
a.outputTaskDetails ();
//与 ·"对象关联"风格的代码:(推荐) 的第二案例对应
//控件"类"
// 父类
function Widget(width,height) {
	this.width = width || 50;
	this.height = height || 50;
	this.$elem = null;
}
Widget.prototype.render = function($where){
	if (this.$elem) {
		this.$elem.css( {
		width: this.width + "px",
		height: this.height + "px"
	} ).appendTo( $where );
}};
// 子类
function Button(width,height,label) {
	// 调用“super”构造函数
	Widget.call( this, width, height );
	this.label = label || "Default";
	this.$elem = $( "<button>" ).text( this.label );
}
// 让 Button“继承”Widget
Button.prototype = Object.create( Widget.prototype );
// 重写 render(..)
Button.prototype.render = function($where) {
	//“super”调用
	Widget.prototype.render.call( this, $where );
	this.$elem.click( this.onClick.bind( this ) );
};
Button.prototype.onClick = function(evt) {
	console.log( "Button '" + this.label + "' clicked!" };
}
$( document ).ready( function(){
	var $body = $( document.body );
	var btn1 = new Button( 125, 30, "Hello" );
	var btn2 = new Button( 150, 40, "World" );
	btn1.render( $body );
	btn2.render( $body );
} );
//ES6的语法糖
class Widget {
	constructor(width,height) {
	this.width = width || 50;
	this.height = height || 50;
	this.$elem = null;
	}
	render($where){
		if (this.$elem) {
			this.$elem.css( {
				width: this.width + "px",
				height: this.height + "px"
			} ).appendTo( $where );
		}
	}
}
class Button extends Widget {
	constructor(width,height,label) {
		super( width, height );
		this.label = label || "Default";
		this.$elem = $( "<button>" ).text( this.label );
	}
	render($where) {
		super( $where );
		this.$elem.click( this.onClick.bind( this ) );
	}
	onClick(evt) {
		console.log( "Button '" + this.label + "' clicked!" );
	}
}
$( document ).ready( function(){
	var $body = $( document.body );
	var btn1 = new Button( 125, 30, "Hello" );
	var btn2 = new Button( 150, 40, "World" );
	btn1.render( $body );
	btn2.render( $body );
} );

3.2 "对象关联"风格的代码:(推荐)

//与 ·"面向对象"风格的代码:(推荐) 的第一案例对应
var Task = {
	setID: function(ID) { this.id = ID; },
	outputID: function() { console.log( this.id ); }
};
// 让 XYZ 委托 Task
var XYZ = Object.create( Task );
XYZ.prepareTask = function(ID,Label) {
	this.setID( ID );
	this.label = Label;
};
XYZ.outputTaskDetails = function() {
	this.outputID();
	console.log( this.label );
};
XYZ.prepareTask("work","7 hours");
XYZ.outputTaskDetails();//work //7 hours

对象关联风格的代码还有一些不同之处。

  1. 在上面的代码中,id 和 label 数据成员都是直接存储在 XYZ 上(而不是 Task)。通常 来说,在 [[Prototype]] 委托中最好把状态保存在委托者(XYZ、ABC)而不是委托目标 (Task)上。
  2. 在类设计模式中,我们故意让父类(Task)和子类(XYZ)中都有 outputTask 方法,这 样就可以利用重写(多态)的优势。在委托行为中则恰好相反:我们会尽量避免在 [[Prototype]]链的不同级别中使用相同的命名,否则就需要使用笨拙并且脆弱的语法 来消除引用歧义。 这个设计模式要求尽量少使用容易被重写的通用方法名,提倡使用更有描述性的方法名,尤其是要写清相应对象行为的类型。这样做实际上可以创建出更容易理解和维护的代码,因为方法名(不仅在定义的位置,而是贯穿整个代码)更加清晰(自文档)。
  3. this.setID(ID);XYZ 中的方法首先会寻找 XYZ 自身是否有 setID(…),但是 XYZ 中并没 有这个方法名,因此会通过 [[Prototype]] 委托关联到 Task 继续寻找,这时就可以找到 setID(…)方法。此外,由于调用位置触发了 this 的隐式绑定规则,因 此虽然 setID(…) 方法在 Task 中,运行时 this 仍然会绑定到 XYZ,这正是我们想要的。 在之后的代码中我们还会看到 this.outputID(),原理相同。 换句话说,我们和XYZ 进行交互时可以使用 Task 中的通用方法,因为 XYZ 委托了 Task。
//与 ·"对象关联"风格的代码:(推荐) 的第二案例对应
//委托控件对象
var Widget = {
	init: function(width,height){
		this.width = width || 50;
		this.height = height || 50;
		this.$elem = null;
	},
	insert: function($where){
		if (this.$elem) {
			this.$elem.css( {
				width: this.width + "px",
				height: this.height + "px"
			} ).appendTo( $where );
		}
	}
};
var Button = Object.create( Widget );
Button.setup = function(width,height,label){
	// 委托调用
	this.init( width, height );
	this.label = label || "Default";
	this.$elem = $( "<button>" ).text( this.label );
};
Button.build = function($where) {
	// 委托调用
	this.insert( $where );
	this.$elem.click( this.onClick.bind( this ) );
};
Button.onClick = function(evt) {
	console.log( "Button '" + this.label + "' clicked!" );
};
$( document ).ready( function(){
	var $body = $( document.body );
	var btn1 = Object.create( Button );
	btn1.setup( 125, 30, "Hello" );
	var btn2 = Object.create( Button );
	btn2.setup( 150, 40, "World" );
	btn1.build( $body );
	btn2.build( $body );
} );

3.3 为什么不推荐"面向对象"风格的代码?有什么弊端?

1.在javascript中,使用行为委托设计(对象关联)比面向对象更加简洁,易懂。 因为javascript中面向对象的语法根源是通过原型链的联系进行的,并非是真正面向“类”的设计。
2.在javascript中,要想实现"继承",机制上来讲,就必须在prototype上添加函数或者变量,prototype仅仅是将两个对象的原型链进行关联,并非是如同面向“类”的语法一般,实现复制父对象至子对象中,因此面向对象的写法是伪造面向对象的写法,并非真正的面向对象。

行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类的关系。JavaScript 的 [[Prototype]]机制本质上就是行为委托机制。也就是说,我们可以选择在 JavaScript 中努 力实现“类”机制,也可以拥抱更自然的 [[Prototype]] 委托机制。 当你只用对象来设计代码时,不仅可以让语法更加简洁,而且可以让代码结构更加清晰。对象关联(对象之前互相关联)是一种编码风格,它倡导的是直接创建和关联对象,不把 它们抽象成类。对象关联可以用基于 [[Prototype]]的行为委托非常自然地实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Funnee

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值