对象(含原型)

目录

1、理解对象概念
2、创建对象的方式

  • 2.1、字面量形式
  • 2.2、工厂模式
  • 2.3、构造函数模式
  • 2.4、原型模式
    3、对象存取属性的方式
    4、属性
  • 4.1、属性的类型
  • 4.2、定义多个属性
  • 4.3、读取属性的特性
    5、合并对象
    6、对象解构

1、理解概念:

对象是一组属性的无序集合,对象的每个属性或方法都有一个key,这个key映射一个值

2、创建对象的方式

2.1、字面量形式
onst obj = {name:111}

2.2、工厂模式
function createPerson(name,age){
  let o = new Object()
	o.name = name
	o.age = age
	o.sayName=function (){console.log(this.name)}
	return o  
}
const person1  = createPerson('haha',20)
const person2  = createPerson('hehe',40)

上述的定义叫做工厂模式,这种方式虽然可以解决批量创建类似对象的问题,但是没有解决对象标识问题(即新创建的对象是什么类型),所以就引申出另一种创建对象的方式

2.3、构造函数模式
function CreatePerson(name,age){
	this.o.name = name
	this..age = age
	this.sayName=function (){console.log(this.name)}
}
const person1  = new CreatePerson('haha',20)
const person2  = new CreatePerson('hehe',40)

上述构造函数模式代替了之前的工厂模式,这两者其实很像,只不过就有如下一些区别:
1、没有在函数内显式的创建对象
2、属性和方法直接赋值给了this
3、没有return
4、构造函数的函数名首字母是大写的,工厂模式函数名是小写

我们要知道在new 构造函数()时内部做了哪些操作:

  1. 在内存中创建了一个新对象
  2. 在这个新对象内部[[ prototype ]] 特性被赋值为构造函数的prototype属性
  3. 构造函数内部的this会指向这个新创建的对象
  4. 然后就是执行构造函数内部的代码(给新对象赋属性和方法)
  5. 最后,如果函数内部没有显式地返回一个对象,那么会自动将这个新对象返回
2.3.1、构造函数与普通函数

他们两者唯一的区别就是调用的方式不同,构造函数也是函数,没有什么特殊的地方,任何函数只要用new调用,那么我们就将这个函数称之为构造函数,不适用new操作符调用的函数就是普通函数,还有一点就是构造函数名首字母要大写,普通函数小写,这也是更好的区别构造函数与普通函数;

2.3.2、构造函数的问题

它主要的问题就是其定义的方法会在每个实例创建时都创建一遍,因为我们知道函数也是对象,所以每次定义方法(函数)时,都会初始化一个对象,逻辑上讲,上述的构造函数实际上是这样的:

function CreatePerson(name,age){
	this.o.name = name
	this..age = age

	// 与刚才的逻辑等价
	this.sayName= new Function('console.log(this.name)') 
}

所以为了解决上述这个重复定义函数的问题,简单的解决方案可以将方法定义在构造函数外面:

function CreatePerson(name,age){
	this.o.name = name
	this..age = age
	this.sayName = sayName
}

// 定义在构造函数外面
function sayName(){
	console.log(this.name)
}

这样虽然解决了相同逻辑的函数重复定义的问题,但全局作用域也因此被搞乱了,因为那个函数实际上只能在一个对象上调用。如果这个对象需要多个方法,那么就要在全局作用域中定义多个函数。这会导致自定义类型引用的代码不能很好地聚集一起。这个新问题可以通过原型模式来解决

2.4、原型模式
2.4.1、理解原型(重点☆)

只要创建一个函数,都会按照特定的规则为这个函数创建一个prototype属性(指向该函数的原型对象),默认情况下,每个原型对象也会自动获得一个constructor的属性,指回与之关联的构造函数

CreatePerson.prototype.constructor 指向 CreatePerson 构造函数

每次调用构造函数会创建一个新实例,这个实例对象内部的[[Prototype]]指针就会被赋值为构造函数的原型对象,由于脚本中没有访问这个[[Prototype]]的标准方式,所以后来在每个实例对象上暴露了__proto__属性,这个属性可以访问构造函数的原型对象

let Person = function(){}
const p1 = new Person()
const p2 = new Person()
p1.__proto__ === Person.prototype   // true

我们来简单梳理下构造函数、实例对象、构造函数的原型对象三者之间的关系:(用刚才的例子)
image.png
接下来整理些方法:

  • isPrototypeOf():接收一个实例对象,它会判断传入的对象的__proto__是否指向调用这个方法的对象(说白了就是检查传入的实例是否是该原型对象的实例)
Person.prototype.isPrototypeOf(person1)    // true
  • Object.getPrototypeOf():返回传入对象内部__proto__的值,也就是返回传入对象的原型对象是谁
Object.getPrototypeOf(person1)    // Person.prototype
  • Object.create():创建一个新对象,传入一个对象作为新对象的原型对象
let biped = {
	numLegs: 2
};
let person = Object.create(biped); // 将biped作为person对象的原型对象
person.name = 'Matt';   
console.log(person.name); // Matt
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true
  • hasOwnProperty():用于确定某个属性是在实例对象上的还是在原型对象上的,如果是实例对象上的,则该方法就会返回true,否则会返回false,注意,属性名是字符串
function Person(){}
Person.prototype.name = 'haha'   // 原型对象上的属性name
const person1 = new Person()
person1.age = '20'
console.log(person1.hasOwnProperty("name"))  // false
console.log(person1.hasOwnProperty("age"))   // true

2.4.2、原型层级

在通过对象访问属性时,会最先从这个实例对象自身开始找,如果在实例对象上找到了,则返回这个值,如果没找到,则会继续往它的原型对象上找,这一原理也就解释了为什么实例对象可以通过constructor找到其对应的构造函数了

const Person =function(){}
const person1 = new Person()
person1.constructor === Person  // true 

其实上面的这个constructor属性是属于Person.prototype的(Person构造函数的原型对象的),person1实例对象中没有该属性就会去其原型对象上找

如果实例对象中存在和原型对象上相同的属性,则会**“遮蔽”**原型对象上的同名属性,但如果你想恢复使用原型对象上的属性的话,只能删除实例对象上的同名属性才行,设置为null也是不行的,返回的还是null,如下例所示:

function Person(){}
Person.prototype.name = 'haha'
const person1 = new Person()
person1.name = 'xixi'
console.log(person1.name)  // xixi
person1.name = null 
console.log(person1.name)  //	null
delete person1.name
console.log(person1.name)  // haha
2.4.3、原型与 “in” 操作符

如果某个属性在该对象上能访问到,则会返回true,无论是在实例对象本身存在还是说其原型对象上存在,只要能访问到就会返回true

function Person(){}
Person.prototype.name = 'haha'
const person1 = new Person()
person1.age = '7'
console.log("age" in person1)   //true   实例对象上存在
console.log("name" in person1)  //true   原型对象上存在

2.4.4、属性的遍历
  1. Object.keys() :遍历实例对象上可枚举的属性(不包含symbol类型的属性),原型对象上的不会输出,但是遍历出来的属性顺序是不一定的,因浏览器而言
  2. Object.getOwnPropertyNames() : 遍历出实例对象上可枚举+不可枚举的属性(除symbol类型的属性外)
  3. for…in循环 : 遍历实例对象和原型对象上可枚举的属性(不包括symbol类型的属性),属性顺序也是不一定的,因浏览器而言
  4. Object.getOwnPropertySymbols() : 遍历出实例对象上所有symbol符号定义的属性名
function Person(){}
Person.prototype.name = 'haha'
const person1 = new Person()
person1.age = 12
const like1= Symbol('like1')
const like2= Symbol('like2')
Person.prototype[like2] = like2
person1[like1] = like1
for(let p in person1){
	console.log(p,person1[p])
} 
// for...in循环输出的结果
	age 12
	name haha
---------------------------------------------------------------------------------
Object.keys(person1)   //  ['age'] 
只遍历实例对象上可枚举的属性(除symbol类型的属性外),原型对象上的不会输出
---------------------------------------------------------------------------------
Object.getOwnPropertyNames(person1)  // ['age']
只遍历实例对象上可枚举+不可枚举的属性(除symbol类型的属性外),原型对象上的不会输出
---------------------------------------------------------------------------------
Object.getOwnPropertySymbols(person1)  //[Symbol(like1)] 
遍历出实例对象上所有的symbol符号定义的属性名,不包含原型对象上的

总结:只有for…in 会遍历原型对象,其他3个方法都是只遍历实例对象本身,而且用symbol定义的属性只能通过Object.getOwnPropertySymbols()方法遍历到,其他3个都不行

2.4.5、其他原型的语法

像前面我们给原型对象添加属性会把Person.prototype重写一遍,这样会比较麻烦,代码看着也会比较冗余,我们则可以这样写:

// 老的写法
function Person(){}
Person.prototype.name = 'haha'
Person.prototype.age = 6
Person.prototype.constructor === Person   // true
-----------------------------------------------------------------------------------------
// 新的写法
function Person(){}
Person.prototype={
	name:'haha',
	age:6,
}
Person.prototype.constructor === Person   // false

代码解析:像上述这样写后虽然写法上简单了不少,但也会存在一个问题,那就是当前原型对象上的constructor不在指向原先的Person构造函数了,为什么呢?
原因:因为原先在创建Person构造函数的时候,会默认创建其prototype对象,并同时给这个prototype对象添加一个constructor属性,指向其构造函数Person,像现在的写法,相当于重写了prototype对象,因此其constructor属性也指向了不同的函数(Object构造函数)

所以我们如果想要用这样的字面量形式去定义原型对象上的属性的话,则还需要再改写下代码:

// 进一步优化写法

function Person(){}
Person.prototype={
	constructor:Person
	name:'haha',
	age:6,
}

代码解析:这么写也还是会有点问题,原生的constructor属性默认是不可枚举的,但你这么写后,你的constructor属性是可枚举的,不完全和原生的一样,所以还得进一步优化下,即用Object.defineProperty()方法将constuctor属性设置为不可枚举

// 最终优化写法

function Person(){}
Person.prototype={
	name:'haha',
	age:6,
}
Object.defineProperty(Person.prototype,'constructor',{
	 enumerable:false,
   value:Person,
})

还有一个重要的知识点:就是要记住,实例对象只有指向原型的指针,没有指向构造函数的指针,如果重新定义了原型对象的话,那么重新定义前的实例对象保存的还是原先的原型对象的指针

function Person() {}
let friend = new Person(); // 创建了实例对象friend
Person.prototype = {    // 重写原型对象
	constructor: Person, 
	name: "Nicholas",
	age: 29,
	job: "Software Engineer",
	sayName() {
		console.log(this.name);
	}
};
friend.sayName();    // 报错

原因:Person 的新实例是在重写原型对象之前创建的。在调用friend.sayName()的时候,会导致错误。这是因为firend 指向的原型还是最初的原型,而这个原型上并没有sayName 属性,用图来表示这之间的关系如下图所示:
image.png

2.4.6、原型模式的问题

原型模式也并非没有问题,它最大的问题就来源与其共享性,原型上的所有属性都是所有实例对象所共享的,对于一些原始值类型的属性还好,如果想要实例之间有各自的值,可以通过在实例上各自重新赋值,前面说到会”遮蔽“原型对象上同名属性的值,但如果是引用类型的属性就会有问题,因为实例对象上保存的也还是引用地址,其中一个实例但凡改变了其属性值就会影响其他实例对象,如下例所示:

function Person() {}
Person.prototype = {
	constructor: Person,
	name: "Nicholas",
	friends: ["Shelby", "Court"],  // 引用类型的属性
	sayName() {
		console.log(this.name);
	}
};
let person1 = new Person();
let person2 = new Person();
person1.friends.push("Van");   // 其中一个实例对象改变了这个值
console.log(person1.friends); // "Shelby,Court,Van"
console.log(person2.friends); // "Shelby,Court,Van"
console.log(person1.friends === person2.friends); // true

总结:所以在实际开发中,通常也是不单独使用原型模式的

3、对象存取属性的方式

  • "."的形式
  • []形式

使用[]访问属性的优势是可以使用变量访问属性

let person = {name:'haha'}
let Myname = 'name' 
console.log(person[myName]) // haha

4、属性

ECMA262使用一些内部特征来描述属性的特征,这些内部特征会用两个中括号包起来,比如:[[ Enumerable ]]

4.1、属性的类型
  • 数据属性:这种属性有4个特征来描述他们的行为
  • 访问器属性:这种属性不包含数据值,但它有一个获取函数和一个设置函数
4.1.1、数据类型:
  • **[[Configurable]]:**表示属性是否可以通过delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是true
  • ** [[Enumerable]]:**表示属性是否可以通过for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是true,如前面的例子所示。
  • **[[Writable]]:**表示属性的值是否可以被修改。默认情况下,所有直接定义在对象上的属性的这个特性都是true,如前面的例子所示。、
  • **[[Value]]:**包含属性实际的值。这就是前面提到的那个读取和写入属性值的位置。这个特性的默认值为undefined

修改对象属性的内部特征:一定要用Object.defineProperty()方法,这个接收3个参数,要给其添加属性的对象、属性的名称和一个描述对象,如下例所示:

let person = {};
Object.defineProperty(person, "name", {
	writable: false,
	value: "Nicholas"
});
console.log(person.name); // "Nicholas"
person.name = "Greg";      // 由于定义name属性时,将[[Writable]]设置为false了,所以不能修改了
console.log(person.name); // "Nicholas"

在调用Object.defineProperty()时,configurable、enumerable 和writable 的值如果不指定,则都默认为false,如下例所示:

let person = {};
Object.defineProperty(person, "name", {
	value: "Nicholas"
});
console.log(person.name); // "Nicholas"
delete person.name
console.log(person.name) 	// "Nicholas" 默认为false,所以不能删除
person,name= 'haha'
console.log(person.name)  // "Nicholas" 默认为false,所以不能修改

4.1.2、访问器属性
  • **[[Configurable]]:**表示属性是否可以通过delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性。默认情况下,所有直接定义在对象上的属性的这个特性都是true。
  • **[[Enumerable]]:**表示属性是否可以通过for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是true
  • [[Get]]:获取函数,在读取属性时调用。默认值为undefined。
  • [[Set]]:设置函数,在写入属性时调用。默认值为undefined、

怎么理解访问属性和数据属性呢?
访问器属性只能通过Object.defineProperty()方法定义,同时你也可以通过Object.defineProperty()方法将原先的数据属性改为访问器属性,也就是去掉[[Value]]和[[Writable]]这两个特征,增加[[Get]]和[[Set]]的函数,在这两个函数中可以重新定于取值和设置值的逻辑

4.2、定义多个属性

ECMAScript 提供了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;
			}
		}
	}
});

4.3、读取属性的特性

前面讲了这么多属性内部特征的定义,那怎么获取这些内部的特征呢?
1、Object.getOwnPropertyDescriptor():该方法可以取得指定属性的属性描述符对象。这个方法接收两个参数:属性所在的对象和要取得其描述符的属性名。返回值是一个对象,对于访问器属性包含configurable、enumerable、get 和set 属性,对于数据属性包含configurable、enumerable、writable 和value 属性的一个对象

let book = {};
Object.defineProperties(book, {
	year_: {
		value: 2017
	},
	edition: {
		value: 1
	},
	year: {
		get: function() {
			return this.year_;
		},
		set: function(newValue){
			if (newValue > 2017) {
				this.year_ = newValue;
				this.edition += newValue - 2017;
			}
		}
	}
});
// descriptor就是获取到的该year_数据属性的特征对象
let descriptor = Object.getOwnPropertyDescriptor(book, "year_");
console.log(descriptor.value); // 2017
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "undefined"
---------------------------------------------------------------
// 这个descriptor就是获取到的该year访问器属性的特征对象
let descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value); // undefined
console.log(descriptor.enumerable); // false
console.log(typeof descriptor.get); // "function"

2、Object.getOwnPropertyDescriptors()这种方法和上面的方法很像,只差一个s,它接收一个参数,就是要查看的对象,它内部其实会在每个自有属性上调用上面的那个方法Object.getOwnPropertyDescriptor,并将所有的数据组成 一个新对象返回

// 按上面的例子其实就是返回下面的内容:
console.log(Object.getOwnPropertyDescriptors(book));
// {
// 	edition: {
//    configurable: false,
//    enumerable: false,
//    value: 1,
//    writable: false
//  },
//  year: {
// 		configurable: false,
// 		enumerable: false,
// 		get: f(),
// 		set: f(newValue),
// 	},
// 	year_: {
// 		configurable: false,
// 		enumerable: false,
// 		value: 2017,
// 		writable: false
// 	}
// }

5、合并对象

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

  • 该方法会修改目标对象,然后返回这个被修改后的目标对象
const dest = {};
const src = { id: 'src' };
const result = Object.assign(dest, src);
// Object.assign 修改目标对象并返回这个被修改后的目标对象
console.log(dest === result); // true
console.log(dest !== src); // true
console.log(result); // { id: src }
console.log(dest); // { id: src }
  • 实际上对每个源对象执行的是浅复制,如果多个源对象都有相同的属性,则使

用最后一个复制的值

const dest = { id: 'dest' };
const result = Object.assign(dest, { id: 'src1', a: 'foo' }, { id: 'src2', b: 'bar' });
// Object.assign 会覆盖重复的属性
console.log(result); // { id: src2, a: foo, b: bar }
--------------------------------------------------------------
// 可以通过目标对象上的设置函数观察到覆盖的过程:
dest = {
	set id(x) {
		console.log(x);
	}
};
Object.assign(dest, { id: 'first' }, { id: 'second' }, { id: 'third' });
// first
// second
// third
----------------------------------------------------------------
dest = {};
src = { a: {} };
Object.assign(dest, src);
// 浅复制意味着只会复制对象的引用
console.log(dest); // { a :{} }
console.log(dest.a === src.a); // true

6、对象解构

ES6新增了对象解构语法

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

解构赋值不一定与对象的属性匹配。赋值的时候可以忽略某些属性,而如果引用的属性不存在,则该变量的值就是undefined:、

let person = {
	name: 'Matt',
	age: 27
};
let { name, job } = person;
console.log(name); // Matt
console.log(job); // undefined  person对象中没有job属性

总结

本文是基于Javascript高级程序设计(第四版)的内容自行总结和梳理,后续还会出一系列关于本书的读后总结博文!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Ronychen’s blog

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

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

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

打赏作者

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

抵扣说明:

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

余额充值