从头认识js-js中的对象

什么是对象?

ECMA-262中把对象定义为:“无序属性的集合,其属性可以包含基本值,对象或者函数”。严格来讲,对象是一组没有特定顺序的值。对象的每个属性或方法·都有一个名字,而每个名字都映射到一个值。

属性类型

1.数据属性

1.[[Configurable]]:表示能够通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。

2.[[Enumerable]]:表示能否通过for-in循环返回属性。

3.[[Writable]]:表示能否修改属性的值。

4.[[Value]]:包含这个属性的数据值。读取属性的值的时候,从这个位置读取;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为undefiend。

要修改属性默认的特性,必须使用ECMAScript5的Object.defineProperty()方法。接受三个参数:属性所在的对象,属性的名字和一个描述符对象(descriptor 对象的属性必须是:configurable,enumerable,writable和value)设置一个或者多个值,可以修改对应的特性。

注意:可以多次调用该方法修改同一个属性,但是把configurable特性设置为false之后就会有限制了。

2.访问器属性

访问器属性不包含数据值:它们包含一对儿getter和setter函数。

1.[[Configurable]]:表示能够通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。

2.[[Enumerable]]:表示能否通过for-in循环返回属性。

3.[[Get]]:在读取属性时调用的函数。默认值为undefined。

4.[[Set]]:在写入属性是调用的函数。默认值为undefeated。

3.定义多个属性

使用Object.defineProperties()方法,可以通过描述符一次定义多个属性。该方法接受两个参数:第一个参数是一个添加或者修改属性的对象,第二个参数是一个对象,该对象的属性与第一个对象中添加或修改的属性一一对应。

4.读取属性的特性

使用Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符,该方法接受两个参数:属性所在的对象和读取去描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有configurable,enumerable,get和set;如果是数据属性,这个对象的属性有configurable,enumerable,writable和value。

创建对象

1.工厂模式

function createPerson(name, age, job) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sauName = function () {
        console.log(this.name);
    }
    return o;
}
var person1 = createPerson('小明', 22, '学生');
var person2 = createPerson('小红', 27, '白领');

函数createPerson每个可以返回一个包含三个属性的对象。这就是典型的工厂模式,工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

2.构造函数模式

像Object和Array的原生构造函数,在运行时会自动出现在执行环境中。此外,我们也可以创建自定义的构造函数,从而定义自定义对象的属性和方法。现在我们使用构造函数模式重写上一个例子。

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

var person1 = new Person('a', 22, '学生');
var person2 = new Person('b', 23, '白领');

我们注意到,Person函数相对以createPerson函数,存在以下不同:

1.没有显示地创建对象。

2.直接将属性和方法赋给了this对象。

3.没有return语句。

Person函数就是一个构造函数,注意到函数Person的首字母是大写的,可以用来创建对象,这也是构造函数区别于其他函数的特征。

要创建Person的新实例,必须使用new操作符。以这种方式调用构造函数实际上会经历以下4个步骤:

1.创建一个新对象。

2.将构造函数的作用域赋予新对象(因此this指向了这个新对象)。

3.执行构造函数中的代码(为这个新对象添加属性和方法)。

4.返回新对象。

person1和person2分别保存着Person的一个不同的实例。这两个对象有一个constructor(构造函数)属性,该属性指向Person。

对象的constructor属性最初是用来标识对象类型的。

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,这也是构造函数模式胜于工厂模式的地方。

构造函数的问题:使用构造函数的主要问题,就是在每个方法都要在每个实例上重新创建一遍。

3.原型模式

每一个函数都有一个propertype(原型)属性,该属性是一个指针,指向一个对象,这个对象的用途是包含可以有特定类型的所有实例共享的属性和方法。如果按照字面量来理解,那么propertype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处就是是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。

1.理解原型对象

创建函数就会为该函数创建一个propertype属性,这个属性指向函数的原型对象。默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性指向propertype属性所在函数的指针。

创建了自定义的构造函数后,其原型对象默认只会取得constructor属性;至于其他方法,则都是从Object继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262第5版中管这个指针叫[[Prototype]]。虽然在脚本中没有标准的方法访问[[Prototype]],但在Firefox,Safari和Chrome在每个对象上都支持一个属性__proto__;而在其他实现中,这个属性是对脚本完全不可见的。注意的是这个连接存在于实例于构造函数的原型对象之间,而不是存在于实例于构造函数之间。

虽然在所有实现中都无法访问到[[Prototype]],但可以通过isPropertypeOf()方法来确定对象之间是否存在这种关系。从本质上来讲,如果[[Propertype]]指向调用isPropertypeOf()方法的构造函数的原型对象,那么这个方法就返回true。

ECMAScript5增加了一个方法,叫Object.getPrototypeOf(),在所有支持的实现中,这个方法返回[[Prototype]]的值,即返回相关的构造函数的原型对象。

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。

虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加了一个属性,而该属性于实例原型中的一个属性同名,那我们就在该实例中创建该属性,该属性将会屏蔽原型中的那个属性。

当对象实例添加一个属性的时候,这个属性会屏蔽原型对象中的保存的同名属性,不过使用delete操作符可以完全删除实例属性,从而让我们能够重新访问原型中的属性。

使用hasOwnProperty()方法可以检测一个属性是否存在于实例中,还是存在原型中,只在给定的属性存在于对象实例中时,才会返回true。

2.原型与in操作符

有两种方式使用in操作符:单独使用和在for-in循环中使用。在单独使用的时候,in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在与实例中还是原型中。

在使用for-in循环时,返回的是所有能够通过对象访问的,可枚举的属性,其中既包括存在于实例中的属性,也包括存在与原型中的原型。屏蔽了原型中不可枚举属性的实例属性也会在for-in循环中返回。

要取得对象上所有可枚举的实例属性,可以使用ECMAScript5的Object.keys()方法。这个方法接受一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。

3.更简洁的原型语法

 之前的原型模式每次创建一个自定义类型的对象的时候,我们都要敲一遍原型,现在我们作出如下修改

function Person() {
}
Person.prototype = {
    name: 'ydb',
    sayName: function () {
        console.log(this.name);
    }
}

这样我们把一个对象字面量赋值给该构造函数的原型,重写了构造函数的默认原型。但是现在construcor不是指向了该构造函数,现在指向了Object构造函数,而且现在constructor变成了可以枚举的属性(构造函数的默认原型对象是不可枚举的)。

现在我们作出如下修改:

function Person() {
}
Person.prototype = {
    name: 'ydb',
    sayName: function () {
        console.log(this.name);
    }
}
Object.defineProperty(Person.prototype, 'constructor', {
    enumerable: false,
    value: Person
})

现在该构造函数的constructor属性变成不可枚举的,而且指向了该构造函数。

4.原型的动态性

先看如下代码:

function Person() {
}
var person1 = new Person();
Person.prototype.doSomethings = function () {
    console.log('ydb');
}
person1.doSomethings();

我们在创建对象实例之后,给对象的原型上添加了方法,则该对象实例是可以使用该函数的,其原因是实例与原型之间的松散关系。当我们使用dosomethings函数的时候,首先会在对象实例上查找,没有找到会继续搜索原型。

5.原生对象的原型

所有原生引用类型(Obkect,Array,Strinhg等等),都在其构造函数的原型上定义了方法。所以我们可以在这些原生引用类型的构造函数的原型上自定义方法。

尽管可以这样子做,但是会导致以下问题:

1.某个实现中缺少某个方法,就在原生对象的原型中添加这个方法,那么当在另一个支持该方法的实现中就可能导致命名冲突

2.而且这样子做,也有可能意外地重写原生方法。

6.原型对象的问题

原型对象的问题如下:

1.它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。

2.由构造函数的原型的共享性质导致的问题。

对于那些只包含基本值的属性,在对象实例化之后,在实例的上添加一个同名属性,就可以屏蔽原型中的对应属性,但是那些包含引用类型的属性,其中的属性是一个指向一种引用类型的指针,当对象实例化之后,实例上的属性也就包含了指向那种引用类型的指针,那么该指针对应的引用类型数据一旦发生改变,所有实例之间的该属性指向的引用类型数据都发生了改变,这样子就会导致所多问题。可是,实例一般要有属于自己的全部属性的。这个问题也是导致很少有人单独使用原型模式创建对象实例的原因。

4.组合使用构造函数模式和原型模式

创建自定义类型的常见方法,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。

function Person(name) {
    this.name = name;
}
Person.prototype.sayName = function () {
    console.log(this.name);
}
var peroson1 = new Person('1');
var peroson2 = new Person('2');

结果,每一个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。可以说,这是用来定义引用类型的一种默认模式。

注意:上述几种依赖原型创建对象实例的模式,如果在已经创建了实例的情况下重写原型,那么就会切断现有实例于新原型之间的联系。因为实例创建之后,其内部的[[Prototype]]指向的是之前的原型对象,这个时候修改之后内部的[[Prototype]]是无法与新原型之间创建连接关系的,所以对象实例可能在调用某些方法或者使用某些属性的时候,导致找不到对应的方法或者属性而报错。

原文地址:https://www.cnblogs.com/jsydb/p/12239045.html