JavaScript 对象的创建与继承——创建篇

本文深入讲解JavaScript中的对象概念,包括对象的创建、使用、进阶属性及Object原型上的方法,帮助读者理解对象作为JS中最基础的数据结构的重要作用。

对象

每一个语言最重要与最基础的数据结构非对象莫属,因为可以通过它实现其他任何数据结构。

什么是对象

在JS语言中,对象的定义未:无序属性的集合,其属性可以包含基本值、对象或者函数,换句话说,就是对象里包含许多属性,这些属性通过映射可以指向任何类型的值。

创建一个对象

  1. 通过实例Object来创建一个对象
var o = new Object()
o.name = 'Rex'
o.age = 23
o.getName = function(){
	return this.name
}
  1. 通过对象字面量来创建一个对象
var rexDate = {
	name:'rex',
	age:23,
	getName:function(){
		return this.name
	}
}

使用对象

JS有2种方法来进行访问对象

  1. 第一种是通过.来进行访问

这个是具有声明变量的功能,如果访问的是对象不存在的属性的时候,返回的是undefined,但是它只是存在于你调用的时候,在你调用完之后,由于JS的垃圾回收机制就会将这个内存进行删除,所以不用担心内存泄漏。这里说一句只是为了区别与我们直接输出声明过的变量是会直接报错的。

var rexDate = {
	name:'rex',
	age:23,
	getName:function(){
		return this.name
	}
}
console.log(rexDate.age)// 23
console.log(rexDate.getName())// rex
  1. 第二种是通过[]来进行访问
var rexDate = {
	name:'rex',
	age:23,
	getName:function(){
		return this.name
	}
}
console.log(rexDate['age'])// 23
console.log(rexDate['getName']())// rex

当然不推荐这样访问对象,这样不仅写起来麻烦阅读性差,并且每一个属性名字都需要加''字符串进行访问,实在太不人性了。
熟悉数组的同学很快就会发现这就是array的访问的方法,所以也可以侧面看出来数组其实本质就是对象,只不过编译器进行封装,我们可以非常方便实用数组。


对象进阶

我们简单熟悉对象之后,也要认识下一些对象底层的一些方法。无论你通过什么方式进行声明对象,最终的实例都会继承Object的原型链上的属性和方法。这里我们就来认识下这些必然会存在的方法和属性(特性)吧!

被忽视的属性(特性)

1. 数据属性:包含一个数据值的位置。在这个位置可以进行读取和写入
  • Configurable属性能否被delete删除,能否被修改特性或者能否把属性修改为访问器的属性。
    稍微解释下,delete是人自行删除释放内存的方法之一,如果设置为false则无法被这个方法删除。而后者则是无法进行修改其他特性和访问器都不能进行设置。这里简单可以理解为属性的权限。
  • Enumerable属性,顾名思义可枚举的,就是设置是否能够被for-in进行枚举
  • Writable属性,就是能否被修改。
  • Value属性,保存的就是属性的值本身——数据值。如果我们只是声明没有进行赋值,则为undefined
    我们直接来测试下吧,文字始终是难记住的,通过例子来强化下记忆!
    我们在声明属性的时候如何同时设置这些属性值,我们不进行任何操作时候他们的默认值是多少?
    通过Object.getOwnPropertyDescriptor(obj, prop)来获取属性配置
var rexDate = {
}
rexDate.name = 'rex'
console.log(Object.getOwnPropertyDescriptor(rexDate, 'name'))

输出情况

{ value: 'rex',
  writable: true,
  enumerable: true,
  configurable: true }

默认情况下,数据属性都是TRUEValue就是数值。
我们继续看下函数的Value是如何保存的?

var rexDate = {
}

rexDate.getName = function() {
  console.log('rex')
};
console.log(Object.getOwnPropertyDescriptor(rexDate, 'getName'))
Object.getOwnPropertyDescriptor(rexDate, 'getName').value()
{ value: [Function],
  writable: true,
  enumerable: true,
  configurable: true }
rex

我们通过访问value直接执行是可以正确返回的,所以value同样是保存数据值的。

尝试下修改这些值会发现什么变化?

通过Object.defineProperty(obj, prop, descriptor)来获取属性配置

  • Configurablefalse时。
var rexDate = {
}
Object.defineProperty(rexDate, 'name',{
  enumerable: true,
  configurable: false,
  writable: true,
  value: "rex"
})
delete rexDate.name
console.log(rexDate.name)
rexDate.name ='Rex'
delete rexDate.name
console.log(rexDate.name)

输出

rex
Rex

无法成功删除,可以修改value,除了configurable之外都是可以重新设置的。
如果我们设置了configurablefalse,如果重新设置为true会报错。

var rexDate = {
}

Object.defineProperty(rexDate, 'name',{
  enumerable: true,
  configurable: false,
  writable: true,
  value: "rex"
})
Object.defineProperty(rexDate, 'name',{
  enumerable: true,
  configurable: true,
  writable: true,
  value: "rex"
})

在这里插入图片描述

  • enumerablefalse
var rexDate = {
}

Object.defineProperty(rexDate, 'name',{
  enumerable: true,
  configurable: true,
  writable: true,
  value: "rex"
})

for(let i in rexDate) {
  console.log(i,rexDate[i])
}
Object.defineProperty(rexDate, 'name',{
  enumerable: false,
  configurable: true,
  writable: true,
  value: "rex"
})
for(let i in rexDate) {
  console.log(i,rexDate[i])
}
console.log('测试完成')

输出

name rex
测试完成

只输出了一行,所以设置了enumerable就无法被for-in遍历了。

  • writablefalse
var rexDate = {
}

Object.defineProperty(rexDate, 'name',{
  enumerable: true,
  configurable: true,
  writable: true,
  value: "rex"
})

rexDate.name = "Rex"
console.log(rexDate.name)
Object.defineProperty(rexDate, 'name',{
  enumerable: true,
  configurable: true,
  writable: false,
  value: "rex"
})
rexDate.name = "Rex"
console.log(rexDate.name)
console.log('测试完成')

输出

Rex
rex
测试完成

设置了false之后,即使修改了也不会保存,但是修改无效。

2. 访问器属性

访问器属性熟悉的同学都知道这是VUE2实现双向绑定的方法,我们就用实现双向绑定来认识访问器属性吧

  • Configurable同上
  • Enumerable同上
  • Get这里设置当访问值时调用的函数。访问方法通过.或者[]
  • Set这里设置是当属性的value数据值发生变化的时候执行的函数。
    直接看例子
<!DOCTYPE HTML>
<html>
<head></head>
<body>
	<input ></input>
	<p id='data'></p>
</body>
<script>
  data ={}
  Object.defineProperty(data,'innerHtml',{
    get:function(v){
      return v
    },
    set:function (params) {
      var pNode = document.querySelector('#data')
      pNode.innerHTML=params
      inputNoed.value=params
    }
  })

  window.onload=function(){
    var inputNoed = document.querySelector('input')
    var pNode = document.querySelector('#data')
    data.innerHtml= '初始化数据'
  }
  // 添加input事件
  inputNoed.addEventListener('change',function(e){
    data.innerHtml = e.target.value
  })

</script>
</html>

输出情况
在这里插入图片描述
这是一个非常简单的demo,vue会复杂很多,不过理解起来也不难。有了这个属性我们就可以通过给属性设置一些函数,让他们具有特殊的限制,比如我们抽奖的时候,累积抽奖可以提高中奖率,但是避免中奖率又不能超过一个数,在开发的时候,我们设置set让它赋值超过20的时候限制只返回20,这样可以防止后续开发者误操作让中奖率设置太高导致损失。

Object原型上的方法

我们这里列举一些比较通用和常见的。

  • Object.assign()通过复制一个或多个对象来创建一个新的对象。
    可以将多个对象进行合并为一个对象,不过这里是浅复制,如果出现相同名字的属性,则以最后一个对象中的重复属性的值为主。

只会拷贝源对象自身的并且可枚举的属性到目标对象,如果设置了enumerablefalse的话是无法复制的。

const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const returnedTarget = Object.assign(target, source);

console.log(target);
// expected output: Object { a: 1, b: 4, c: 5 }

console.log(returnedTarget);
// expected output: Object { a: 1, b: 4, c: 5 }

我们通过绕下路,将属性中的对象变成字符串,在进行合并,合并之后将字符串变为对象,这样就可以实现深复制

面试的时候用这个回答会很加分。

// Deep Clone 
obj1 = { a: 0 , b: { c: 0}}; 
let obj3 = JSON.parse(JSON.stringify(obj1)); 
obj1.a = 4; 
obj1.b.c = 4; 
console.log(JSON.stringify(obj3)); // { a: 0, b: { c: 0}}
  • Object.create()使用指定的原型对象和属性创建一个新对象。
var obj1 = {
  name :'rex'
}
var obj2 = Object.create(obj1)
console.log(obj2, obj2.name)
var obj3 = new Object(obj1)
console.log(obj3, obj3.name)

输出

{} 'rex'
{ name: 'rex' } 'rex'

create进行创建的方法并不是直接保存到返回的对象上的,而是保存到对象的prototype原型上,所以可以访问到name
等价于

var obj = function () {
  
}
var test = {name:'rex'}
obj.prototype = test
var o = new obj()
console.log(o, o.name)
console.log(o.__proto__  === obj.prototype)
console.log(o instanceof obj)

输出情况

{} 'rex'
true
true

new Object()等价于

var obj = function () {this.name = 'rex'}
obj2 = new obj()
/*
	var o = new Object()
	var bindObj = obj.bind(o)
	bindObj()
	return o
*/
// 可以看看下面的代码输出什么

var test = function () {
  this.name = 'rex'
}
var o = new Object()
var bindO = test.bind(o)
bindO()
console.log(o) // {name : 'rex'}

我们用一张图来解释下这里的区别
在这里插入图片描述
我们在寻找属性的时候,如果实例没有对应的属性,会通过访问prototype来继续访问,如果没有则继续访问prototypeprototype直到Object的原型上,如果没有则返回undefined
我们看下Object.create是如何进行创建的?
在这里插入图片描述
我们继续看new的过程
在这里插入图片描述
我们这里来证明下这个图

let obj = function (params) {
  this.name = 'rex'
}

let o = new obj()
console.log(o.prototype == obj)
console.log(o.__proto__ == obj.prototype)
console.log(o.__proto__.constructor == obj)
console.log(o.constructor == obj)

输出

false
true
true

实例的__proto__的确指向这构造函数的原型,构造函数constructor这个属性保存的是构造函数本身,new过程中绑定的函数就是它,通过输出o.__proto__.constructor == obj可以清楚地看到,obj并不是原型本身,而是原型上构造函数constructor指向的函数,而构造函数本身存在一个prototype属性指向这原型。有点难理清楚,我们画张图表示下就明白了。
在这里插入图片描述

  • Object.defineProperty()设置属性和属性的配置,Object.defineProperties()是同时设置多个属性的配置

  • Object.entries()方法返回一个给定对象自身可枚举属性的键值对数组,这里可以阅读阮一峰的ES6教程里的遍历器

  • Object.fromEntries(iterable);参数有键值对的列表参数。

    map转对象

    const map = new Map([ ['foo', 'bar'], ['baz', 42] ]);
    console.log(map)// Map { 'foo' => 'bar', 'baz' => 42 }
    const obj = Object.fromEntries(map);
    console.log(obj); // { foo: "bar", baz: 42 }
    

    数组转对象

    const arr = [ ['0', 'a'], ['1', 'b'], ['2', 'c'] ];
    console.log(arr) // [ [ '0', 'a' ], [ '1', 'b' ], [ '2', 'c' ] ]
    const obj = Object.fromEntries(arr);
    console.log(obj); // { 0: "a", 1: "b", 2: "c" }	
    
  • Object.getPrototypeOf()在实例中,我们需要通过__proto__来访问实例的prototype,也可以使用这个方法获取实例的prototype

const prototype1 = {};
const object1 = Object.create(prototype1);
console.log(object1.__proto__ === prototype1);// true
console.log(Object.getPrototypeOf(object1) === prototype1);
// expected output: true

  • Object.freeze()方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。
    下面是ES6拓展的方法
  • Object.getOwnPropertyNames(obj)
    返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
  • Object.getOwnPropertySymbols(obj)返回一个数组,包含对象自身的所有 Symbol 属性的键名。
let obj = {
	name:'a',
	age:'b',
	[Symbol('123')]:123
}
console.log(Object.getOwnPropertyNames(obj))
console.log(Object.getOwnPropertySymbols(obj))

输出

[ 'name', 'age' ]
[ Symbol(123) ]
  • Object.setPrototypeOf设置prototype
Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

设计一个对象

我们从上面可以了解到,通过构造函数可以给每一个实例添加属性和方法,实例也可以继承构造函数的原型对象,这2个唯一的区别就是前者每一个实例都是独立的,而后者每一个实例都是共享的。

function Person(name,age){
	this.name = name
	this.age = age
}
Person.prototype.sayname= function(){
	console.log(this.name)
}
Person.prototype.sex = 'mela'
var p1 = new Person('rex',12)
var p2 = new Person('jone',23)
console.log(p1.sayname())// rex
console.log(p2.sayname())// rex
console.log(p2.sex,p1.sex )// mela mela
p1.sex = 'femela'
console.log(p2.sex,p1.sex )// femela femela

构造函数保存每个实例需要单独保存的方法。
原型保存所有实例的共享方法和属性。

参考链接

更多Object方法和详细内容请浏览MDN
《Javascript高级程序设计》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值