充分了解JavaScript中的对象,顺便弄懂你一直不明白的原型和原型链

引言

相信不少小伙伴学习了javascript,可是对于对象只是有一个初步的认识,而且对于对象的原型和原型链之类的概念尚未很好的认识吧,本篇文章带大家一步步了解对象。本篇文章一共13000+的字,我真的废了不少不少的心思,但愿大家耐心的看完,而且能有深一层的理解。javascript

正文

相信不少小伙伴都是冲着原型和原型链的概念进来的,那么大家能够先看正文的第二部分,而后再看第三部分,就能充分理解原型和原型链的概念了java

1、对象的定义

对象是JavaScript的基本数据类型,对象内部是由一个个的名/值对组成的,例以下面咱们常见的。python

{
	name: '张三',
	fn: function() { return 1 }
}

在JavaScript中,对象一共有三类,分别是:web

  1. 内置对象:是由ECMAScript定义的对象或类。例如数组 Array 、函数 Function 、日期 Date 、正则表达式 RegExp
  2. 宿主对象:由JavaScript解释器所嵌入的宿主环境定义的。例如浏览器提供的对象 Window 、Document
  3. 自定义对象:是由用户写的JavaScript代码建立的对象。例如let obj = {}

2、对象的建立

建立对象的方式一共有三种,分别是:正则表达式

  1. 对象直接量
  2. 经过new建立对象
  3. 经过Object.creat()建立对象
  • 对象直接量

这种建立方式是咱们最多见的,也是最经常使用的数组

let obj1 = {}      //建立了一个空的对象
let obj2 = {	   //建立了有一个属性为name,值为张三的对象
	name: '张三'
}

这种方式建立对象有一种缺点,好比在一个会重复调用的函数里,用了对象直接量的方式建立对象, 会重复建立很对的新对象,而且每次建立的对象的属性值也有可能不一样。因此在实际应用中,若是遇到此类状况,尽可能避免使用对象直接量的方式建立对象。浏览器

  • 经过new建立对象

这种建立对象的方式,通常都是一个new运算符后面跟随一个函数调用,而且该函数有一个名字,叫作构造函数,顾名思义,就是用于构造建立一个对象。svg

let arr = new Array()
let data = new Date()

这几个都是经过new调用了一个内置对象的构造函数,建立了新的对象实例,咱们其实也能够本身定义一个构造函数,而后也经过new的方式来调用咱们自定义的构造函数来建立一个对象实例,例如函数

function MyObj() {
	
}
let obj = new MyObj()    //成功调用构造函数 MyObj,建立了一个对象
  • 经过Object.create()建立对象

这种方式是ES5中规定的新的建立对象的一种方式,只须要简单的传一个原型对象,便可建立对应的新的对象,例如学习

let obj = Object.create({x:1, y:2})  //{x:1, y:2}是obj的原型

3、对象的原型以及原型链

刚在讲解建立对象的方法时,咱们在介绍Object.creat()时,提到了原型的概念,在这里咱们就来解释一下,什么是对象的原型。

每个对象(除了null)都和另外一个对象有关联,“ 另外一个对象 ” 就叫作原型。 在这里咱们能够作个形象的比喻,将原型比做一家餐饮店,将对象比做这家餐饮店的加盟店,这样理解起来就很容易了,咱们建立了一个对象,就至关于咱们开这家餐饮店的加盟店,而且咱们的食材配方 、经营方法都是来自于这家加盟店,因此这里咱们能够引入继承的概念,说是加盟店继承于这家餐饮店。

在JavaScript中,绝大部分的对象都有一个共同的原型,他就是 Object.prototype ,也就是说 Object.prototype 是最原始的那家餐饮店,而非加盟店。

每一个函数的内部都有一个属性,叫作 prototype,他表示该函数的原型对象,例如

//先写一个构造函数
function MyObj() {
	//这里暂时先不写任何代码
}
console.log(typeof MyObj)   // function
console.log(typeof MyObj.prototype)  // Object

从这个例子中能够看出,MyObj.prototype 返回的是一个对象类型的值,因此这就表示了它是该构造函数的原型对象。

每一个对象(除了null)都有一个属性——__proto__ ,该属性表示的是该对象的原型,咱们来举两个例子,这两个例子是分别不一样方式建立对象后,展现他们各自的原型

  • 用对象直接量建立的对象的原型
let arr = []                          //对象直接量建立对象
console.log(arr.__proto__ === Array.prototype)    // true
console.log(arr.__proto__.__proto__ === Object.prototype)  //true
console.log(arr.__proto__.__proto__.__proto__)  // null

arr.__proto__ 表示的是arr这个对象的原型,而咱们都知道对象直接量实际上是一种语法糖的写法,在这个例子中 let arr = [] 间接调用了 new Array ,因此咱们能够经过 Array.prototype 来表示 Array 这个构造函数的原型对象, 将 arr.__proto__Array.prototype 作比较,发现它俩相等,因此 arr 的原型就是构造函数Array的原型对象。

那么 arr.__proto__.__proto__ 是为了表示 arr 的原型(Array.prototype)的原型,由于咱们上面说过,绝大多数的对象都有一个共同的原型 Object.prototype , 因此咱们判断一下 Array.prototype 的原型是否就是 Object.prototype,从结果来看,确实是的。

最后咱们再去寻找 Object.prototype 的原型(arr.__proto__.__proto__.__proto__)就找不到了,由于 Object.prototype 咱们看做是一家餐饮店的源头啊,他并非谁的加盟店,因此最终返回 null。

在这里咱们就能够引入一个概念,叫作原型链。顾名思义,就是一条链子上有不少的原型,如图
在这里插入图片描述

  • 经过Object.create()建立对象
let arr = Object.create({x:1, y:2})
console.log(arr.__proto__)          // {x:1, y:2}
console.log(arr.__proto__.__proto__ === Object.prototype) // true
console.log(arr.__proto__.__proto__.__proto__)    // null

在第三种建立对象方式中,咱们说到,用Object.create()建立对象,只须要传入一个原型对象,就能够建立一个继承于该原型对象的新对象。 因此 arr 的原型(arr.__proto__)就是咱们传入的那个对象,即 {x:1, y:2}

arr.__proto__.__proto__ 返回的是 {x:1, y:2} 的原型。咱们都知道{x:1, y:2} 这样的对象是经过对象直接量建立的,因此他实际上是经过 new Object() 来建立的对象, 那么 {x:1, y:2} 的原型就为 Object.prototype 了。

一样的, Object.prototype 没有原型,因此最后返回 null 。

此时的原型链是这样的,如图
在这里插入图片描述

4、对象的属性

定义: 一个对象内部的每一个名/值对就是该对象的一个属性,例如 {x:1 ,y:2} 中, x:1 就是该对象的一个属性。

属性有两种类型:

  1. 自有属性: 直接定义在对象中的属性,例如 let obj = {x:1}中,属性x就是该对象的自有属性
  2. 继承属性: 在对象的原型中定义的属性,例如 Object.prototype 中的属性 toString

属性特性一共有四种:

  1. 值: 顾名思义,表示该属性的值
  2. 可写性: 表示是否能够设置该属性的值
  3. 可枚举性: 表示是否能够经过 for / in 循环返回属性的值
  4. 可配置性: 表示是否能够删除或修改该属性

注意:这里列举了 属性的类型属性的特性 ,在下面讲解属性的相关知识时,都会涉及到,因此你们请先尽力记住,这对下面的理解有帮助。

(1)属性的查询与设置

当咱们要设置或者获取一个对象的属性时,咱们通常都是这么作的

let obj = {
	name: 'Lpyexplore',
	age: 21,
	gender: 0
}

let name = obj.name     //获取obj对象中的name属性 Lpyexplore
let age = obj["age"]    //获取obj对象中的age属性 21
let fn = obj.toString   //获取obj对象中的继承属性toString [Function: toString]
 
obj["like"] = "python"  //给obj对象设置一个like属性,值为python
obj.weight = 128        //给obj对象设置一个weight属性,值为128
obj.age = 22            //修改obj对象中的age属性,值改成22

console.log(obj.height) //获取obj对象中的height属性
console.log(obj)        //查看一下obj对象的变化
/* undefined { name: 'Lpyexplore', age: 22, gender: 0, like: 'python', weight: 128 } */

从这个例子中咱们能够看到,查询一个对象的属性有两种方式,第一种是经过点(.)来访问到某属性,即 对象.属性名 ; 第二种是经过方括号([ ])来访问某属性,即 对象[属性名]。 当访问对象中不存在的属性时,会返回undefined

那么给对象设置一个属性值就更简单了,查询到该属性值后,直接给它赋值就能够设置属性值(若对象内不存在该属性)或修改属性值(对象内已存在该属性值)

(2)属性的删除

删除对象的自有属性,须要用到运算符 delete,直接来看例子

let obj = {
    x:1,
    y:2,
    z:3
}

delete obj.x         //删除对象obj中的属性x,返回true
delete obj["y"]      //删除对象obj中的属性y,返回true
delete obj.toString  //toString是对象obj的原型中的属性,属于继承属性,没法删除该属性,但仍然返回true

console.log(obj.x)   //查询对象obj中的属性x
console.log(obj)     //查看对象obj

/* undefined {z:3} */

在这个例子中能够看到,咱们准备删除对象obj中的继承属性toString时,未作任何操做,因此 delete 只能删除对象的自有属性

(3)属性的检测

咱们有时须要检测对象的属性,即判断该对象中是否有某个属性 、该属性是否为该对象的自有属性 、该对象是不是可枚举的等等

  • 经过 in 运算符判断属性是否存在
let obj = {
	x: 1,
	y: undefined
}

"x" in obj        //返回true,表示对象obj中有属性x
"y" in obj        //返回true,表示对象obj中有属性y,只不过值为undefined
"z" in obj        //返回false,表示对象obj中不存在属性z
delete obj.x      //删除对象obj中的x属性
"x" in obj        //返回false,表示对象obj中不存在属性x
  • 经过对象的 hasOwnProperty( )方法判断属性是否为自有属性
let obj = {
	x:1
}

obj.hasOwnProperty("x")     //返回true,代表属性x存在,且为obj的自有属性
obj.hasOwnProperty("z")     //返回false,属性zu不存在于对象obj中
obj.hasOwnProperty("toString")  //返回false,属性toString是obj的继承属性,不是自有属性
  • 经过对象的 propertyIsEnumerable( )方法判断属性是否为自有属性,且该属性具备可枚举性
let obj = Object.create({x:1}) //新建一个对象obj,继承于对象 {x:1}
obj.y = 2                   //给obj对象设置一个属性y,其值为2

obj.propertyIsEnumerable("y")  //返回true,表示属性y为该对象的自有属性,且具备可枚举性
obj.propertyIsEnumerable("x")  //返回false,由于属性x是继承属性,继承于对象{x:1}
Object.prototype.propertyIsEnumerable("toString")   //返回false,虽然属性toString是对象Object.prototype的自有属性,但它不具备可枚举性

这里提到了可枚举性, 咱们来举个例子

let obj = {
	x:1, 
	y:2,
	z:3
}

for(let i in obj) {    //遍历对象obj的自有属性
	console.log(i)
}

// 输出 1 2 3

能够看到对象obj的自有属性都被 for / in 所有遍历了出来,这就是可枚举性的体现。

其实在ES5中,提供了两个便利属性的函数,咱们来了解一下

  • Object.keys( )

这个函数是会返回一个数组,数组中的元素就是对象中可枚举的自有属性名,来看一下例子

let obj = {
    x:1,
    y:2,
    z:3
}

Object.keys(obj)   // ['x', 'y', 'z']
  • Object.gerOwnPropertyNames( )

这个函数与Object.keys() 相似,区别就在于,该函数返回的是对象中全部自有属性的名称,即无论属性是否具备可枚举性都能被返回。

let obj = {
    x:1,
    y:2,
    z:3
}

Object.keys(obj)   // ['x', 'y', 'z']

注意: 由于咱们尚未讲到如何将一个属性变为可枚举或变为不可枚举,因此这两个函数的区别没办法很好的体现,接下来咱们就来说解一下如何设置属性的三个特性,其中也包括如何设置可枚举性。等了解完如何设置属性的可枚举性后,咱们再来尝试一下这两个函数的区别,应该就很好理解了。

(4)特殊的属性

常见的对象属性通常都是名/值对的形式,即 x:1 这样的,咱们把这种形式的属性叫作数据属性。在ES5中,提供了一种新的属性形式,叫作存储器属性,该属性能够用两种方法定义,他们分别是 getter 和 setter ,存储器属性在对象中的存在形式不是名/值对的样子,而是相似于咱们平时定义函数的样子,function fn() {}。定义存储器属性就是用get(getter)或set(setter) 代替关键字 function,fn就是该属性的名字,有点抽象,来看例子吧

let obj = {
	x:1,
	y:2,
	get r() {     //用getter方法定义了属性r,在查询该属性时,调用该函数
		return this.x + 2		
	},
	set r(data) {   //用setter方法定义了属性r,在给属性r赋值时,调用该函数,并将值做为该函数的参数
		this.y * data;
		return this.y
	}
}

let r = obj.r        //查询对象obj中的属性r,返回 3
obj.r = 7            //给对象obj的属性r赋值为7, 返回 14

从上面这个例子中能够得出如下的结论

  1. 用getter方法定义了属性,在查询该属性值时,会调用getter方法定义的函数名为该属性的函数
  2. 用setter方法定义了函数,在给该属性赋值时,会调用setter方法定义的函数名为该属性的函数
  3. 经过前两条结论,能够知道,若是一个存储器属性具备getter方法,则该属性可读;若是具备setter方法,则该属性可写;同时拥有两个方法的话,则该属性是一个可读写的属性。

(5)属性的特性

在第四部分的开头,咱们说了属性有四个特性,即值 、可写性 、可枚举性 、可配置性,忘记了的小伙伴翻到前面再看一下。

通常咱们建立的数据属性,都是具备这四个特性的(值 、可写性 、可枚举性 、可配置性), 存储器属性是不具备值和可写性两个特性的,但他也具备四个特性,他们分别为:读取(get)、写入(set)、可枚举性 和 可配置性。

在这里咱们先引入一个概念,也是ES5定义的一个对象,叫作属性描述符,这个对象就表明了属性的四个特性。属性描述符对象里的属性有 value(值)writable(可写性)enumerable(可枚举性)configurable(可配置性)get(读取)set(写入)

经过 Object.getOwnPropertyDescriptor( ) 能够得到一个对象中某个属性的属性描述符。该方法第一个参数为对象,第二个参数为须要查询的该对象中的属性名。接下来咱们来实战一下

  • 查看数据属性的属性描述符
//先建立一个对象obj
let obj = {
	x:1
}

Object.getOwnPropertyDescriptor(obj, "x")    //查询对象obj中属性x的属性描述符

// 返回 {value: 1, writable: true, enumerable: true, configurable: true}
  • 查看存储器属性的属性描述符
//先建立一个对象obj
let obj = {
	get r() {
		return this.x + 1
	},
	set r(data) {
		this.x = data
	}
}

Object.getOwnPropertyDescriptor(obj, "r")   //查询对象obj中属性r的属性描述符

// 返回 {get: [Function: get r], set: [Function: set r], enumerable: true, configurable: true}

getOwnPropertyDescriptor()只能获取到一个属性的属性描述符,若是咱们想要修改某个属性的特性的话,咱们须要用到另外一个方法,即 Object.defineProperty() ,他的第一个参数是对象;第二个参数是须要建立或者修改的属性名;第三个参数是属性描述符对象。

直接来看两个实战例子

  • 修改对象中属性的特性
//建立一个对象
let obj = {
	x:1
}

//先用propertyIsEnumerable()来测试一下对象obj中的属性x是否还具备可枚举性
obj.propertyIsEnumerable("x")   //返回true,说明此时属性x是具备可枚举性的

//修改对象obj中属性x的属性特性
Object.defineProperty(obj, "x", {
	value: 2,                //属性x的值变为2
	writable: true,          //属性x具备可写性
	enumerable: false,       //属性x不具备可枚举性
	configurable: true       //属性x具备可配置性
})

//利用 propertyIsEnumerable()来测试一下对象obj中的属性x是否还具备可枚举性
obj.propertyIsEnumerable("x")      // 返回 false,说明对象obj中属性x已经不具备可枚举性了
  • 给对象建立一个属性,并设置该属性的特性
//建立对象obj
let obj = {
	x:2
}

//给对象obj建立一个属性,并配置好该属性的特性
Object.defineProperty(obj, "r", {
	get: function() {return this.x + 1},      //给存储器属性r设定一个get函数
	set: function(data) {this.x *= data}, //给存储器属性r设定一个set函数
	enumerable: true,             //存储器属性r具备可枚举性
	configurable: true            //存储器属性r具备可配置性
})

//查询对象obj中的属性r
obj.r                // 返回 3

//给对象obj中的属性r赋值
obj.r = 3  

//查询对象obj中的属性x的值
obj.x        //返回 6 
  • 给对象同时建立多个属性,并为每一个属性配置属性特性

这里要用到另外一个方法,即 Object.defineProperties() ,这个方法跟 Object.defineProperty() 相似。前者一共有两个参数,第一个参数为对象;第二个参数为一个对象,而且该对象内部是以名/值对的形式存在的,即 须要修改的属性名: 属性描述符对象。接下来咱们直接来看实战例子

let obj = {
	x:1
}

Object.defineProperties(obj, {
	x: {
		value: 3,
		writable: true,
		enumerable: true,
		configurable: true
	},
	y: {
		value: 10,
		writable: true,
		enumerable: true,
		configurable: true
	}
})

console.log(obj)

// 返回 {x:3, y:10}

在这里简单总结一下

  1. Object.defineProperty() 这个方法有两个做用,第一个是修改属性的特性;第二个做用就是给一个对象建立属性
  2. Object.defineProperty()Object.getOwnPropertyDescriptor() 能够搭配着使用,尤为是在你不知道一个属性的特性的时候,能够先用后者获取它的属性描述符对象,再根据属性描述符对象调用前者去修改属性的特性。
  3. 不管是 Object.defineProperty() 仍是 Object.defineProperties() ,他们都只能修改对象的自有属性,没法修改他们的继承属性。

5、对象的特性

对象一共有三个对象特性,他们分别是:

  1. 对象的原型: 每一个对象(除了null)都与另外一个对象相关联,而且继承另外一个对象的属性或方法。
  2. 对象的类: 是一个标识对象类型的字符串
  3. 对象的扩展标记: 指明了是否能够向该对象添加新的属性

(1)对象的原型

在上面我已经花必定的篇幅去介绍了对象的原型了,这里就再也不作过多的介绍了。这里再介绍两种判断原型的方法,第一个是 Object.getPrototypeOf(),第二个是 ifPrototypeOf()

  • Object.getPrototypeOf( )

这个方法是ES5新增的方法,它须要传入对象做为参数,而后就会返回这个对象的原型,来看一下例子

let obj = Object.create({x:1})   //新建一个对象obj继承于 {x:1}

Object.getPrototypeOf(obj)       //返回 {x:1}
  • isPrototypeOf( )

该方法能够判断一个对象是不是另外一个对象的原型,或者说在另外一个对象的原型链上。 来看一下例子

let obj1 = {x:1}
let obj2 = Object.create(obj1)

obj1.isPrototypeOf(obj2)         //返回true,由于obj2就是继承于obj1

Object.prototype.isPrototypeOf(obj2)  //返回true,由于Object.prototype在obj2的原型链上

(2)对象的类

对象也是有类型的,这个我在本文的开头也就列举了,对象一共有这三大类,分别是内置对象 、宿主对象 、自定义对象。

咱们如何来区分对象的具体类型呢?其实在 Object.prototype 中定义了一个方法属性,叫作 toString,调用该方法,并传入一个对象,就会返回一个字符串,字符串里的信息就用以表示对象的类型。

咱们直接经过 Object.prototype.toString.call(obj) 来判断对象的类型,这里用到的 call 的做用就是将 toString 方法内部的 this 指向咱们要判断的对象,若是有不懂的小伙伴能够去查看一下个人另外一篇介绍call的文章

//先将 Object.prototype.toString.call() 封装成一个函数,方便后面的代码简化
function classof(obj) {
	return Object.prototype.toString.call(obj)
}

classof(null)                         //[object Null]
classof(undefined)                    //[object Undefined]
classof(1)                            //[object Number]
classof("")                           //[object String]
classof(true)                         //[object Boolean]
classof({})                           //[object Object]
classof([])                           //[object Array]
classof(/\d+/)                        //[object RegExp]
classof(new Date())                   //[object Date]
classof(window)                       //[object window]
classof(document)                     //[object HTMLDocument]
function a() {}
classof(new a())                      //[object Object]
let b = {}
classof(b)                            //[object Object]
let c = Object.create(b)
classof(c)                            //[object Object]

简单总结一下

  1. 继承于内置对象的对象,会返回一个字符串,字符串里包含了该内置对象的构造函数名
  2. 判断宿主对象的类型时,例如判断window对象的类型,须要在浏览器环境下才能判断。
  3. 经过对象直接量 、调用构造函数(new fn())以及Object.create() 这三种方式建立的自定义对象,他们的对象类型都为 [object Object]

(3)对象的扩展

对象是具备扩展性的,其表示可否给对象添加新的属性。通常状况下,对象都是可扩展的,除非咱们将他转换成了不可扩展的。接下来咱们来看几个将对象转换成不可扩展的方法。

  • Object.preventExtensions( )

咱们要想将一个可扩展的对象转换成不可扩展的,咱们须要调用 Object.preventExtensions() ,将须要转换的对象做为参数传入便可。

注意

  1. 对象一旦转换成不可扩展,就没法再转成可扩展的了
  2. 对象转换成不可扩展,只会影响该对象自己,但不会影响原型。即没法给该对象添加新的属性,可是咱们能够给该对象的原型添加新的属性,该对象能够继承原型中的新属性。

好了废话很少说,接下来看一个例子

let obj1 = {x:1}
let obj2 = Object.create(obj1)     //obj2继承于Obj1
obj2.z = 4                         //给obj2添加一个属性z,值为4
console.log(obj2)                  //打印 {z:4}
Object.preventExtensions(obj2)     //将obj2转换成不可扩展的
obj2.y = 2                         //尝试向obj2中添加新属性y
console.log(obj2.y)                  // 打印 undefined ,说明obj2是不可扩展的

obj1.y =  2
console.log(obj2.y)          // 打印 2 ,说明obj2虽然被转换成不可扩展的了,可是仍是能从原型继承属性的

delete obj2.z                 //打印 {},说明使用Object.preventExtensions()将对象转换成不可扩展后,虽然没法添加属性,可是能够删除属性

咱们能够将对象传入 Object.isExtensible() 来判断对象是否为可扩展的。

  • Object.seal( )

该方法是ES5中提供的,不只具备能将对象转换成不可扩展的做用(没法添加新属性),并且还能使对象中的自有属性都设置为不可配置的,即没法删除自有属性了。咱们能够成为将对象封闭

let obj1 = {x:1}
let obj2 = Object.create(obj1)     //obj2继承于Obj1
obj2.z = 4                         //给obj2添加一个属性z,值为4
console.log(obj2)                  //打印 {z:4}
Object.seal(obj2)                  //将obj2封闭

delete obj2.z                 //打印 {z:4},说明使用Object.seal()将对象封闭后,既没法添加属性,也没法删除属性

obj2.z = 100                   //修改obj2中的属性z为100
console.log(obj2)              //打印 {z:100} ,说明已封闭的对象,仍是能够对其属性进行修改的

咱们能够将对象传入 Object.seal() 中,判断对象是否已封闭

  • Object.freeze( )

该方法也是ES5中提供的,它在 Object.seal() 的基础之上,增长了一个限制,即将对象的自有的全部数据属性设置为只读。咱们把这种限制叫作冻结

let obj1 = {x:1}
let obj2 = Object.create(obj1)     //obj2继承于Obj1
obj2.z = 4                         //给obj2添加一个属性z,值为4
console.log(obj2)                  //打印 {z:4}
Object.freeze(obj2)                  //将obj2冻结

obj2.z = 100                   //修改obj2中的属性z为100
console.log(obj2)              //打印 {z:4} ,说明已冻结的对象,没法修改对象中的属性值

咱们能够将对象传入 Object.isFrozen() 中,判断对象是否已冻结

结束语

好了,洋洋洒洒一整篇文章也写完了,写加上思考理解大概耗时有十几个小时吧,说真的,刚开始写这篇文章的时候,本身对于 “对象” 这个概念也不是说很透彻很透彻,可是我为了让你们能明白,本身也花了不少的心思去研究,去理解。到如今完结,我对 “对象” 的概念也是有了更深的理解了。 真的是原创不易啊,你们以为写的不错的点个关注,点个赞,感谢啦~