JavaScript中的对象(一):面向对象

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/weixin_44196299/article/details/100071213

与其他语言相比,JavaScript中的“对象”总是显得不那么合群,首先它不像其他语言那样有类的概念(直到ES6),另外JavaScript中可以给对象中自由地添加属性而其他语言不行,甚至关于JavaScript是一门面向对象还是基于对象的语言也饱受争议,这是因为基于对象和面向对象两个形容词都出现在JavaScript标准的各个版本中,在JavaScript标准中关于基于对象的定义如下:语言和宿主的基础设施由对象来提供,并且JavaScript程序即是一系列相互通讯的对象集合。这里的意思根本不是表达弱化的面向对象的意思,反而是表达对象对于语言的重要性,因此实际上JavaScript还是一门面向于对象的弱类型语言,接下来就让我们一起共同探究JavaScript中的对象。

一、什么是对象

在共同研究JavaScript中的对象之前我们不妨探讨一个更本质的问题,什么是对象?
维基百科上对于对象的定义为:

在计算机科学中,对象(Object)是一个存储器地址,其中拥有值,这个地址可能有标识符指向此处。对象可以是一个变量,一个数据结构,或是一个函数。是面向对象(Object Oriented)中的术语,既表示客观世界问题空间(Namespace)中的某个具体的事物,又表示软件系统解空间中的基本元素。
在软件系统中,对象具有唯一的标识符,对象包括属性(Properties)和方法(Methods),属性就是需要记忆的信息,方法就是对象能够提供的服务。在面向对象(Object Oriented)的软件中,对象(Object)是某一个类(Class)的实例(Instance)。

对象是系统中用来描述客观事物的一个实体,它是构成系统的一个基本单位。一个对象由一组属性和对这组属性进行操作的一组服务组成。从更抽象的角度来说,对象是问题域与实现域中某些事物的一个抽象,它反映该事物在系统中需要保存的信息和发挥的作用;它是一组属性和有权对这些属性进行操作的一组服务的封装体。客观世界是由对象和对象之间的联系组成的。

基于对象的定义,Grandy Booch在《面向对象分析与设计》中将对象特点总结为以下三点:

  • 对象具有唯一标识性:即使完全相同的两个对象也并非同一个对象。
  • 对象有状态:对象具有状态,同一对象可能处于不同的状态之下。
  • 对象具有行为:即对象的状态可能因为它的行为产生变迁。

我们首先来看对象具有唯一标识性,一般而言各种语言的对象唯一标识性都是用内存地址来体现的,对象具有唯一标识的内存地址,所以具有唯一的标识。实际上JavaScript中任何的不同的对象其实也是互不相等的,以下面代码为例:

var o1 = { a: 1 };
var o2 = { a: 1 };
console.log(o1 === o2) // false

关于对象的特征和行为不同的语言有不同的术语和抽象来描述它们,比如C++中称它们为“成员变量”和“成员函数”,Java中则将它们称为属性和方法,而在JavaScript中,将状态和行为统一抽象为“属性”,这主要考虑到JavaScript中将函数设计为一种特殊的对象,所以JavaScript中的行为和状态都能用属性来抽象。
在实现了对象基本特征的基础上, JavaScript中对象独有的特色是:对象具有高度的动态性,这是因为JavaScript赋予了使用者在运行时为对象添改状态和行为的能力。
示例:

var o = { a: 1 };
o.b = 2;
console.log(o.a, o.b); // 1 2

为了提高抽象能力,JavaScript的属性被设计成比别的语言更加复杂的形式,它提供了数据属性和访问器属性(getter/setter)两类。

二、JavaScript对象的属性

JavaScript对象的属性分为数据属性和访问器属性,但对于JavaScript来说,属性并非只是简单的名称和值,JavaScript用一组特征(attribute)来描述属性(property)。
数据属性包含一个数据值的位置,在这个位置可以读取和写入值,总共有4个描述行为的特性

  • [[writable]]:表示能否修改属性的值。默认值为true
  • [[Enumerable]]:表示能否通过for in循环返回属性。代表属性是否可以枚举。直接在对象上定义的属性默认值为true
  • [[configurable]]:表示是否能通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。直接在对象上定义的属性,他们的默认值为true
  • [[value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读取。写入属性值的时候,把新值保存在这个位置。这个特性的默认值为undefined

我们通常用于定义属性的代码会产生会产生数据属性,其中的writable、enumerable、configurable都默认为true,我们可以使用内置函数Object.getOwnPropertyDescriptor()方法来查看,如以下代码所示:

var o = { a: 1 };
o.b = 2;
// a,b都是数据属性
Object.getOwnPropertyDescriptor(o, "a") 
// { configurable: true, enumerable: true, value: 1, writable: true }
Object.getOwnPropertyDescriptor(o, "b")
// { configurable: true, enumerable: true, value: 2, writable: true }

如果我们想要改变属性的特征,我们可以使用Object.defineProperty()方法,,这个方法接收三个参数:属性所在的对象、属性的名字、和一个描述符对象(也就是上面的四个数据属性,用来描述对象属性的特性),这里要注意了,采用Object.defineProperty方法创建属性时候,数据属性[[configurable]]、[[writable]]、[[enumerable]]默认为false,这要和字面量直接声明属性时默认值相反,但是如果只是用Object.defineProperty改变原来已有属性的值则没有此限制,示例:

Object.defineProperty(obj,'x',{value:1});
//和下面的是一样的
Object.defineProperty(obj,'x',{
    value:1,
    writable:false,
    enumerable:false,
    configurable:false
});

这儿我们注意如果把configurable设置为false,表示不能删除对象属性,对这个这个属性调用delete方法,非严格模式下什么也不会发生,严格模式下则会抛出错误,而且一旦把属性从configurable设置为false,以后就不能把它设置为true,此时在调用Object.defineProperty方法修改除writable、value之外的特性,都会导致错误,看下面例子:

var obj = {};
Object.defineProperty(obj, 'a', {
	configurable: false,
    enumerable: true,
    writable: true,
    value: 1,
});
Object.defineProperty(person, 'name', {
    // configurable: true,  //会报错!
    // enumerable: false,  //会报错!
    writable: false,
    value: 2,
});
obj.a = 3;
console.log(obj.a); // 2

描述对象的enumerable属性称为“可枚举性”,如果该属性为false,就表示某些操作会忽略当前属性,ES5有三个操作会忽略enumerable属性为false的属性:

  • for…in循环:只遍历对象自身的和继承的可枚举属性。
  • Object.keys():返回对象自身的所有可枚举属性的键名。
  • JSON.stringfy():只串行化对象自身的可枚举属性。

ES6新增了一个操作Object.assign(),会忽略enumerable为false的属性,只复制对象自身的可枚举属性。
这4个操作之中,只有for…in会返回继承的属性,实际上,引入enumerable的最初目的就是为了让某些属性可以规避掉for…in操作。比如,对象原型的toString方法以及数组的length属性,就通过这种手段而不会被遍历到。
示例:

Object.getOwnPropertyDescriptor(Object.prototype,  'toString').enumerable 
// false

另外,ES6规定,所有class的原型的方法都是不可枚举的。
示例:

Object.getOwnPropertyDescriptor(class{foo()}.prototype, 'foo').enumerable
// false

访问器属性使得属性在读和写时执行代码,它使得使用者在写和读属性时得到完全不同的值,它可以视为一种函数的语法糖,包括get获取属性的值;set设置属性的值,它也有四个描述行为的特性:

  • getter:函数或undefined,在取属性值时被调用
  • setter:函数或undefined,在设置属性值时被调用
  • enumerable:决定for in 能否枚举该属性
  • configurable:决定该属性能否被删除或者改变特征值

示例:

var o = { 
	get a() { return 1 } 
};
console.log(o.a); // 1

这里的a就是对象o的访问器属性,访问器属性跟数据属性不同,每次访问属性都会执行getter或者setter函数。这里我们的getter函数返回了1,所以o.a每次都得到1。
我们也可以通过Object.defineProperty()定义访问器属性,示例:

var book = { 
    _year: 2018,
    edition: 1,
};
Object.defineProperty(book, 'year', {
   get: function () {
      return this._year;
   },
   set: function (newValue) {
      if (newValue > 2004) {
         this._year = newValue;
         this.edition += newValue - 2004;
      }
   }
});
console.log(book.year)  // 2018
//通过对象方法访问,调用getter函数;
book.year = 2005;
console.log(book.edition); 
//2
console.log(book._year)
// 2005

补充:
1、当数据属性的configurable和writable的值发生变化时,会产生不一样的情况,具体情况如下表所示:
在这里插入图片描述
我们一起来看看表中标记*的具体例子:

Object.defineProperty(obj1, 'x', {
    value: 1,
    writable: false,
    enumerable: false,
    configurable: true
});
obj1.x = 2;
console.log(obj1.x); // 1
Object.defineProperty(obj1, 'x', {
    value: 3,
    writable: true
});
console.log(obj1.x);//3
obj1.x = 5;
console.log(obj1.x);//5
//如果属性不可配置,但是可以把writable的true变成false,但不能将false变为true

Object.defineProperty(obj1, 'y', {
    value: 1,
    writable: true,
    enumerable: false,
    configurable: false
});
obj1.y = 6;
console.log(obj1.y);//6
Object.defineProperty(obj1, 'y', {
    writable: false
});
obj1.y = 10;
console.log(obj1.y);//6

2、如果我们有多个属性值需要设置应该怎么办?
答案是Object.defineProperties()方法,这个方法有两个参数:第一个要定义属性的对象,第二个也是对象,表示要添加的多个属性和其对应的属性描述符,示例:

var person = {};
Object.defineProperties(person,{
    'name': {
        value:'xiaozhang',
        writable:true,
        enumerable:true,
        configurable:true
    },
    _age: {
        value:21,
        writable:true
    },
    age: {
    	get: function () {
    		return this._age;
    	},
    	set: function(newValue) {
    		this._age = newValue-1;
    	}
    }
});
person.sex = 'Man';
console.log(person.name); // xiaozhang
console.log(person.age); // 21
person.age = 23;
console.log(person._age); // 22

// 得到对象属性的描述
console.log(Object.getOwnPropertyDescriptor(person,'name'));
// { configurable: true, enumerable: true, value: "xiaozhang", writable: true,  __proto__: Object }
console.log(Object.getOwnPropertyDescriptor(person,'_age'));
// { configurable: false, enumerable: false, value: 22, writable: true,  __proto__ : Object }
console.log(Object.getOwnPropertyDescriptor(person,'age'));
// { enumable: false, configurable: false, get: f(), set: f(newValue), __proto__ : Object }

实际上JavaScript 对象在运行时本质上是一个“属性的集合”,属性以字符串或者Symbol为key,以数据属性特征值或者访问器属性特征值为value。对象是一个属性的索引结构,而能够以Symbol(ES6引入)为属性名,这是JavaScript对象的一个特色。
至此我们也可以更好地回答为什么会有JavaScript到底是基于对象还是面向对象的争论:JavaScript的对象设计跟目前主流基于类的面向对象差异非常大。而事实上,这样的对象系统设计虽然特别,但是JavaScript提供了完全运行时的对象系统,这使得它可以模仿多数面向对象编程范式(例如基于类和基于原型),所以它也是正统的面向对象语言。JavaScript语言标准也已经明确说明,JavaScript是一门面向对象的语言,我想标准中能这样说正因为JavaScript的高度动态性的对象系统。

那么JavaScript中的对象又有那些特性呢?

三、JavaScript对象的特性

与JavaScript对象相关的特性有三个:对象的原型(prototype)、对象的类(class)、对象的扩展标记(extensible flag)
对象的原型指向另外一个对象,本对象的属性继承自它的原型对象,

  • 通过对象字面量创建的对象使用Object.prototype作为它们的原型
  • 通过new创建的对象使用构造函数的prototype属性作为他们的原型
  • 通过Object.create()创建的对象使用第一个参数(也可以是null)作为它们的原型

那么到底什么是原型?
1、prototype本质上还是一个JavaScript对象;
2、每个函数都有一个默认的prototype属性;
3、通过prototype我们可以扩展Javascript的内建对象

对象的类是一个标识对象类型的字符串
(关于对象的类和原型将在我的JavaScript中的对象系列博客中进行详细介绍,此处将重点讲解对象的扩展性)

对象的扩展标记指明了(在ECMAScript5中)是否可以向该对象添加新属性
JavaScript中的对象相较于其他语言的一个独有优势就是对象具有高度的动态性,所有内置对象和自定义对象都是显示可扩展的,宿主对象的可扩展性是由JavaScript引擎定义的。
可以通过Object.preventExtensions()将对象设置为不可扩展的,而且不能再转换成可扩展的了,可以通过Object.isExtensible()检测对象是否是可扩展的。示例:

var obj = {};
//检测对象是否可扩展
console.log(Object.isExtensible(obj)); // true
var d = new Date();
console.log(Object.isExtensible(d)); // true
obj.x = 1;
console.log(obj.x); // 1
// 通过preventExtensions()将对象变为不可扩展的
obj1 = Object.preventExtensions(obj);
console.log(obj === obj1); // true
console.log(Object.isExtensible(obj1)); / / false
obj1.y = 2;
console.log(obj1.y); // undefined

//通过下面的这种方法会报错
Object.defineProperty(obj1, 'z', {
    value: 1
});

preventExtensions()只影响到对象本身的可扩展性,如果给一个不可扩展的对象的原型添加属性,这个不可扩展的对象同样会继承这些新属性,同时对象的可扩展性并不会影响到对象的属性,因此需要注意修改对象可扩展性的目的是将对象锁定,防止外接干扰,通常和对象的属性的可配置性与可写性配合使用。

问题:如何把对象变为不可扩展的,且保持对象自身的属性变为不可修改的?
Object.seal()和Object.preventExtensions()类似,除了能够将对象设置为不可扩展的,还可以将对象的所有自身属性都设置为不可配置的。也就是说不能给这个对象添加新属性,而且它已有的属性也不能删除或配置,不过它已有的可写属性依然可以设置。可以通过Object.isSealed()检测对象是否封闭。
示例:

var obj = {
    x: 1,
    y: 2,
};
delete obj.x;
console.log(obj.x); // undefined
var o = Object.seal(obj);
console.log(Object.isSealed(o)); // true,被封闭了
console.log(obj === o); // true

// 封闭后对象不可扩展,并且自身属性不可删除或配置
console.log(Object.isExtensible(o)); // false,不可扩展的
obj.y = 55; // 对象属性仍然可以修改,这是因为属性的writable依然为true
console.log(obj.y); // 55 
/*访问器属性是不可以修改的,运行下面代码会报错
    Object.defineProperty(obj,'y',{
        get :function(){
            return 4;
        }
    });
*/
o.z = 77;
console.log(o.z); // undefined
// Object.defineProperties(obj,'z',{value:'5'}); // 报错
// Object.defineProperties(o,'z',{value:'5'}); // 同样会报错
console.log(o.y); // 2
delete o.y;
console.log(o.y); // 2
console.log(Object.getOwnPropertyDescriptor(obj, 'y'));
// { value: 2, writable: true, enumerable: true, configurable: false }

Object.freeze()将更严格地锁定对象–冻结(frozen)。除了对象设置为不可扩展的和将其属性设置为不可配置的之外,冻结对象的所有自身属性都不可能以任何方式被修改。 任何尝试修改该对象的操作都会失败, 可能是静默失败, 也可能会抛出异常( 严格模式中)数据属性的值不可更改, 访问器属性( 有getter和setter) 也同样( 但由于是函数调用, 给人的错觉是还是可以修改这个属性)。 可以使用Object.isFroze()来检测对象是否冻结。

示例:

var person = {};
Object.defineProperties(person,{
    name: {
        value:'xiaozhang',
        writable:true,
        enumerable:true,
        configurable:true
    },
    _age: {
        value:21,
        writable:true
    },
    age: {
     	get: function () {
      	   return this._age;
        },
        set: function(newValue) {
             this._age = newValue;
        }
    }
});
var obj = Object.freeze(person);
console.log(obj === person); // true
console.log(Object.isFrozen(obj)); // true
// 对象已经被冻结,所有下述操作都是失效的
obj.sex = 'Man';
console.log(obj.sex); // undefined
delete obj.name;
console.log(obj.name); // xiaozhang
obj.name = 'zhang';
console.log(obj.name); // xiaozhang
obj.age = 25;
console.log(obj.age); // 21

Object.freeze()方法可以冻结对象自身的属性,但是当对象的属性为对象时又有所不同,示例:

var obj1 = {
    internal: {}
};
//冻结obj1
Object.freeze(obj1);
//在属性中添加值1
obj1.internal.x = 1;
console.log(obj1.internal.x);//1

我们发现如果一个属性的值是一个对象,而这个对象的属性是可以修改的,这是因为Object.freeze()方法实际上是保证对象属性变量指向的那个内存地址不得改动,对于简单类型的数据(数值、字符串、布尔值)而言,值就保存在变量指向的内存地址中,因此等同于常量,但是对于复合类型的数据(主要是对象和数组)而言,变量指向的内存地址保存的只是一个指针,Object.freeze()只能将这个指针冻结却不能保证这个指针指向的数据结构是不是发生变化,我们把这种情况称之为浅冻结。要使对象不可变,需要递归冻结每个类型为对象的属性,称为深冻结。

//递归的冻结(深度冻结)
function deepFreeze(obj) {
    var prop, propKey;
    Object.freeze(obj);
    for(propKey in obj) {
        //如果还是一个对象的话,将obj[propKey]赋值给prop
        prop = obj[propKey];
        //如果没有自己的属性或者不是一个Object或者冻结这个函数
        if(!obj.hasOwnProperty(propKey) || !(typeof prop === 'object') || Object.isFrozen(prop)) {
            continue;
        }
        deepFreeze(prop);
    }
}

var obj2 = {
    internal: {}
};
deepFreeze(obj2);
obj2.internal.x = 1;
console.log(obj2.internal.x);//undefined

至此,我们对JavaScript中对象的扩展性总结如下:
1、默认对象是可扩展的,也就是非冻结的

 console.log(Object.isFrozen({})); // false

2、一个不可扩展的空对象同时也是一个冻结的对象(必须为没有属性的空对象)

var obj=Object.preventExtensions({});
console.log(Object.isFrozen(obj)); // true

3、一个非空对象默认是非冻结

var obj1={x:1};
console.log(Object.isFrozen(obj1)); // false
Object.preventExtensions(obj1); // 将对象变成不可扩展的
console.log(Object.isFrozen(obj1)); // false
delete obj1.x; // 删掉属性之后,变成可冻结的了
console.log(Object.isFrozen(obj1)); // true

4、如果一个不可扩展的对象拥有一个访问器属性,它也是非冻结的

var obj2={
    get test(){
        return 1;
    }
};
Object.preventExtensions(obj2);
console.log(Object.isFrozen(obj2)); // false,非冻结的
Object.defineProperty(obj2,'test',{configurable:false});
console.log(Object.isFrozen(obj2)); // true,冻结的

5、直接冻结一个对象,那么Object.isSealed()的检测效果也为true

var obj3={x:1};
Object.freeze(obj3);
console.log(Object.isFrozen(obj3)); // true,被冻结了
console.log(Object.isSealed(obj3)); // true,密封的
console.log(Object.isExtensible(obj3)); // true,不可扩展的

补充:到目前为止JavaScript一共有5种方法可以遍历对象的属性:
1、for…in:
for…in循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)
2、Object.keys(obj):
Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)
3、Object.getOwnPropertyNames(obj):
Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是不包括不可枚举属性)
4、Object.getOwnPropertySymbols(obj):
Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有Symbol属性。
5、Reflect.ownKeys(obj):
Reflect.ownKeys返回一个数组,包含对象自身的所有属性,不管属性名是Symbol还是字符串,也不管是否可枚举。

参考资料:极客时间《重学前端》专栏

猜你喜欢

转载自blog.csdn.net/weixin_44196299/article/details/100071213