JavaScript高级程序设计学习(4)

我们跳过内置对象那块的api使用部分,直接学习对象相关内容,等对象,函数部分学习完毕后,再来看内置数据类型,对象,集合,迭代器等相关知识。

八,对象、类与面向对象编程

8.1 理解对象

创建自定义对象的通常方式是创建 Object 的一个新实例,然后再给它添加属性和方法,就好比要创造一个人,需要定义从人这个对象创建一个真实的人,并设定性别,年龄等等。

let person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";
person.sayName = function() {
 console.log(this.name);
};

不过现在普遍使用下面的方法创建对象

let person = {
 name: "Nicholas",
 age: 29,
 job: "Software Engineer",
 sayName() {
 console.log(this.name);
 }
};

小疑问:为什么这里可以直接创建而不用通过new创建呢?
答:JavaScript引擎会识别{}表示一个新的对象,并将其赋值给变量。这个过程是在运行时(runtime)发生的。JavaScript引擎会隐式地创建一个新的对象,并在内部调用Object构造函数来创建这个对象,因此你无需显式地使用new Object()来创建对象。

8.1.1 属性的类型

这里说的属性是JavaScript内置属性,开发者不能在 JavaScript 中直接访问这些特性。为了将某个特性标识为内部特性,规范会用两个中括号把特性的名称括起来,比如[[Enumerable]]。
属性分两:数据属性和访问器属性。
1.数据属性
数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。数据属性有 4个特性描述它们的行为。

  • [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。
  • [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。
  • [[Writable]]:表示属性的值是否可以被修改。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。
  • [[Value]]:包含属性实际的值。这就是前面提到的那个读取和写入属性值的位置。这个特性的默认值为 undefined。
let person = {
 name: "Henry"
}; 

这里,我们创建了一个名为 name 的属性,并给它赋予了一个值"Nicholas"。这意味着
[[Value]]特性会被设置为"Henry",之后对这个值的任何修改都会保存这个位置。而其他的几个数据属性默认为true。
要修改属性的默认特性,就必须使用 Object.defineProperty()方法。

let person = {};
Object.defineProperty(person, "name", {
 writable: false,
 value: "Nicholas"
});
console.log(person.name); // "Nicholas"
person.name = "Greg";
console.log(person.name); // "Nicholas" 

其使用方法一目了然,接受三个参数,第一个为修改的对象目标,第二个为对象的属性名,第三个为一个对象,里面可以选填数据属性。
这里面有一个特殊的地方在于,可以对同一个属性多次调用 Object.defineProperty(),但在把 configurable 设置为 false 之后就会受限制了。
2. 访问器属性
访问器属性不包含数据值。相反,它们包含一个获取(getter)函数和一个设置(setter)函数。访问器属性有 4 个特性描述它们的行为:

  • [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特
    性,以及是否可以把它改为数据属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。
  • [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。
  • [[Get]]:获取函数,在读取属性时调用。默认值为 undefined。
  • [[Set]]:设置函数,在写入属性时调用。默认值为 undefined。
    访问器属性同样要通过 Object.defineProperty()来定义。
// 定义一个对象,包含伪私有成员 year_和公共成员 edition
let book = {
 year_: 2017,
 edition: 1
};
Object.defineProperty(book, "year", {
 get() {
 	return this.year_;
 },
 set(newValue) {
 	if (newValue > 2017) {
 		this.year_ = newValue;
 		this.edition += newValue - 2017;
 	}
 }
});
book.year = 2018;
console.log(book.edition); // 2 

这里有个需要注意的地方:
如果使用了Object.defineProperty()定义数据属性,那么数据属性的除value的其他三个特性为false,如果没有使用Object.defineProperty()来定义数据属性,那么就是true。

8.1.2 定义多个属性

Object.defineProperties()方法。这个方法可以通过多个描述符一次性定义多个属性。它接收两个参数:要为之添加或修改属性的对象和另一个描述符对象,其属性与要添加或修改的属性一一对应。如

let book = {};
Object.defineProperties(book, {
 	year_: {
 	value: 2017
 },
 edition: {
 	value: 1
 },
 year: {
 	get() {
 		return this.year_;
 	},
 	set(newValue) {
 		if (newValue > 2017) {
 			this.year_ = newValue;
 			this.edition += newValue - 2017;
 		}
 	}
 }
});

8.1.3 读取属性的特性

使用 Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符。这个方法接
收两个参数:属性所在的对象和要取得其描述符的属性名。返回值是一个对象,对于访问器属性包含configurable、enumerable、get 和 set 属性,对于数据属性包含 configurable、enumerable、writable 和 value 属性。如

let descriptor = Object.getOwnPropertyDescriptor(book, "year_");
console.log(descriptor.value); // 2017
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "undefined"
let descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value); // undefined
console.log(descriptor.enumerable); // false
console.log(typeof descriptor.get); // "function" 

当然类似defineProperty和defineProperties方法,getOwnPropertyDescriptor也有对应获取多个的api,用 Object.getOwnPropertyDescriptor(),该方法接收一个参数,返回值是一个包含该对象所有访问器属性和数据属性的对象集合。

8.1.4 合并对象

Object.assign()这个方法接收一个目标对象和一个或多个源对象作为参数,然后将每个源对象中可枚举(Object.propertyIsEnumerable()返回 true)和自有(Object.hasOwnProperty()返回 true)属性复制到目标对象。对每个符合条件的属性,这个方法会使用源对象上的[[Get]]取得属性的值,然后使用目标对象上的[[Set]]设置属性的值。如

let dest, src, result;
/**
 * 简单复制
 */
dest = {};
src = { id: 'src' };
result = Object.assign(dest, src);
// Object.assign 修改目标对象
// 也会返回修改后的目标对象
console.log(dest === result); // true
console.log(dest !== src); // true
console.log(result); // { id: src } 

注意,第一个参数是目标对象,第二个之后的对象是源对象(需要合并的内容),当然该api是会影响到目标对象的。
Object.assign()实际上对每个源对象执行的是浅复制。如果多个源对象都有相同的属性,则使
用最后一个复制的值。此外,从源对象访问器属性取得的值,比如获取函数,会作为一个静态值赋给目标对象(基础数据类型深拷贝,引用数据类型浅拷贝)。不能在两个对象间转移获取函数和设置函数。
如果赋值期间出错,则操作会中止并退出,同时抛出错误。Object.assign()没有“回滚”之前
赋值的概念,因此它是一个尽力而为、可能只会完成部分复制的方法。

8.1.5 对象标识及相等判定

ECMAScript 6 规范新增了 Object.is(),这个方法与===很像,但同时也考虑到了一些边界情形。

console.log(Object.is(true, 1)); // false
console.log(Object.is({}, {})); // false
console.log(Object.is("2", 2)); // false
// 正确的 0、-0、+0 相等/不等判定
console.log(Object.is(+0, -0)); // false
console.log(Object.is(+0, 0)); // true
console.log(Object.is(-0, 0)); // false
// 正确的 NaN 相等判定
console.log(Object.is(NaN, NaN)); // true 

8.1.6 增强的对象语法

主要介绍一些定义和操作对象的语法糖。
1. 属性值简写
当对象的属性名称和变量名一样的时候可以简写。
2. 可计算属性
在之前对象字面量中直接动态命名属性,因为对象的属性名其实是字符串,有个可计算属性,就可以在对象内通过中括号包围告诉运行时将其作为JavaScript表达式而不是字符串。

const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let person = {
 [nameKey]: 'Matt',
 [ageKey]: 27,
 [jobKey]: 'Software engineer'
};

3. 简写方法名

let person = {
 sayName(name) {
 	console.log(`My name is ${name}`);
 }
};
person.sayName('Matt'); // My name is Matt 

当然匿名函数无法做到上述例子。

8.1.7 对象解构

let person = {
 name: 'Matt',
 age: 27
};
let { name, age } = person;
console.log(name); // Matt
console.log(age); // 27

如果没有相应匹配的属性,那么获取到的值就是undefined。当然也可以在解构的时候赋默认值,和函数的参数默认值一样。

let person = {
 name: 'Matt',
 age: 27
};
let { name, job='Software engineer' } = person;
console.log(name); // Matt
console.log(job); // Software engineer

只要是对象内可以找到的属性,都可以解构赋值,这里就包括原型链上面的属性,如

const { call } = () => {}

在解构的上下文中,原始数据类型会被当成对象,但根据规定,null和 undefined 不能被解构。
解构并不要求变量必须在解构表达式中声明。不过,如果是给事先声明的变量赋值,则赋值表达式必须包含在一对括号中:

let personName, personAge;
let person = {
 name: 'Matt',
 age: 27
};
({name: personName, age: personAge} = person);
console.log(personName, personAge); // Matt, 27 

当然解构赋值对于引用类型也是浅拷贝。
解构赋值可以使用嵌套结构,以匹配嵌套的属性:

let person = {
 	name: 'Matt',
 	age: 27,
 	job: {
 		title: 'Software engineer'
 	}
};
// 声明 title 变量并将 person.job.title 的值赋给它
let { job: { title } } = person;
console.log(title); // Software engineer

这里理解成一直嵌套的通过“.”获取属性即可。
2. 部分解构
和Object.assign一样,报错后不会回滚,整个解构赋值只会完成一部分。
3. 参数上下文匹配
在函数参数列表中也可以进行解构赋值。对参数的解构赋值不会影响 arguments 对象

let person = {
 name: 'Matt',
 age: 27
};
function printPerson(foo, {name, age}, bar) {
 console.log(arguments);
 console.log(name, age);
}
function printPerson2(foo, {name: personName, age: personAge}, bar) {
 console.log(arguments);
 console.log(personName, personAge);
}
printPerson('1st', person, '2nd');
// ['1st', { name: 'Matt', age: 27 }, '2nd']
// 'Matt', 27
printPerson2('1st', person, '2nd');
// ['1st', { name: 'Matt', age: 27 }, '2nd']
// 'Matt', 27 

8.2 创建对象

虽然使用 Object 构造函数或对象字面量可以方便地创建对象,但这些方式也有明显不足:创建具有同样接口的多个对象需要重复编写很多代码。(引出原型的概念)

8.2.1 概述

8.2.2 工厂模式

下面的例子展示了一种按照特定接口创建对象的方式

function createPerson(name, age, job) {
	 let o = new Object();
	 o.name = name;
	 o.age = age;
	 o.job = job;
	 o.sayName = function() {
	 	console.log(this.name);
	 };
	 return o;
}
let person1 = createPerson("Nicholas", 29, "Software Engineer");
let person2 = createPerson("Greg", 27, "Doctor"); 

这种工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题。(即新创建的对象是什么类型)

8.2.3 构造函数模式

ECMAScript 中的构造函数是用于创建特定类型对象的。像 Object 和 Array 这样的原生构造函数,运行时可以直接在执行环境中使用。当然也可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。当一个函数被new操作符修饰后,该函数就为构造函数,构造函数的通过new操作符返回的值就是对象。

function Person(name, age, job){
	 this.name = name;
	 this.age = age;
	 this.job = job;
	 this.sayName = function() {
	 	console.log(this.name);
	 };
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); // Nicholas
person2.sayName(); // Greg

另外,要注意函数名 Person 的首字母大写了。按照惯例,构造函数名称的首字母都是要大写的,非构造函数则以小写字母开头。
要创建 Person 的实例,应使用 new 操作符。以这种方式调用构造函数会执行如下操作。

  1. 在内存中创建一个新对象。
  2. 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。
  3. 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
  4. 执行构造函数内部的代码(给新对象添加属性)。
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

上一个例子的最后,person1 和 person2 分别保存着 Person 的不同实例。这两个对象都有一个constructor 属性指向 Person。
constructor 本来是用于标识对象类型的。不过,一般认为 instanceof 操作符是确定对象类型
更可靠的方式。前面例子中的每个对象都是 Object 的实例,同时也是 Person 的实例,如下面调用instanceof 操作符的结果所示:

console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true 

实例化的时候,如果没有参数,可以直接通过new操作符赋值

let person2 = new Person;

1. 构造函数也是函数
构造函数与普通函数唯一的区别就是调用方式不同。除此之外,构造函数也是函数。任何函数只要使用 new 操作符调用就是构造函数,而不使用 new 操作符调用的函数就是普通函数。
2. 构造函数的问题
构造函数的主要问题在于,其定义的方法会在每个实例上都创建一遍。因此对前面的例子而言,person1 和 person2 都有名为 sayName()的方法,但这两个方法不是同一个 Function 实例。因为都是做一样的事,所以没必要定义两个不同的 Function 实例。
要解决这个问题,可以把函数定义转移到构造函数外部:

function Person(name, age, job){
	 this.name = name;
	 this.age = age;
	 this.job = job;
	 this.sayName = sayName;
}
function sayName() {
 	console.log(this.name);
} 

在这里,sayName()被定义在了构造函数外部。在构造函数内部,sayName 属性等于全局 sayName()函数。因为这一次 sayName 属性中包含的只是一个指向外部函数的指针,所以 person1 和 person2共享了定义在全局作用域上的 sayName()函数。这样虽然解决了相同逻辑的函数重复定义的问题,但全局作用域也因此被搞乱了,因为那个函数实际上只能在一个对象上调用。如果这个对象需要多个方法,那么就要在全局作用域中定义多个函数。这会导致自定义类型引用的代码不能很好地聚集一起。这个新问题可以通过原型模式来解决。

  • 19
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值