对象

1.对象的定义


“无序属性的集合,其属性可以包含基本值,对象或者函数”。每个属性都是一个名/值对。属性名是字符串,因此我们可以把对象看成是从字符串到值的映射。


2.对象的创建

=======

通过new创建对象
new运算符创建并初始化一个新对象。关键字new后跟随一个函数调用,这个函数被称为构造函数。

var person = new Object();    //创建一个空对象

person.name = "Jack";         //添加属性
person.age = 20;

person.sayName = function(){  //添加方法
    alert(this.name);
}

对象字面量
对象字面量是一个表达式。每次运算都创建并初始化一个新的对象。每次计算对象直接量的时候,也都会计算它的每个属性的值。

var person = {
    name: "Jack",
    age: 20,
    sayName: function(){
        alert(this.name);
    }
};

原型
每一个JS对象(null除外)都与另一个对象相关联。“另一个”对象就是我们所熟知的原型,每一个对象都从原型继承属性和方法。
所有通过对象字面量创建的对象都具有同一个原型对象,可以通过Object.prototype获得对原型对象的引用。通过关键字new和构造函数调用创建的对象的原型就是构造函数的prototype属性的值。
Object.prototype没有原型,不继承任何属性。

Object.create()
ES5定义了一个名为Object.create的方法,他创建一个新对象,其中第一个参数是这个对象的原型,第二个参数用以对对象的属性进行进一步描述。
可以通过任意原型创建新对象,换句话说,可以使任意对象可继承。

var o1 = Object.create(Object.prototype);

var o2 = Object.create(null);        //不继承任何属性和方法

new一个对象
扩展:new一个对象时做了什么?
举例:使用new关键字创建对象new ClassA()

1.创建一个空对象

var obj = {};

2.设置新对象的__proto__指向构造函数的原型对象

obj.__proto__ = ClassA.prototype;

3.将构造函数的作用域赋给新对象

ClassA.call(obj);

4.执行构造函数中的代码并返回新对象

3.属性类型


数据属性
数据属性包含一个数据值的位置。在这个位置可以读取和写入值。
[[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。
[[Enumerable]]:表示能否通过 for-in循环遍历属性。
[[Writable]]:表示能否修改属性的值。
[[Value]]:包含这个属性的数据值。

访问器属性
访问器属性不包含数据值,包含一对儿getter和setter函数(非必需)。访问器属性不能直接定义,要通过Object.defineProperty()来定义。
[[Get]]:在读取属性时调用的函数。
[[Set]]:在写入属性时调用的函数。

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

读取属性的特性
使用 ES5 的 Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象。


4.创建对象的模式

使用小结1中的方法创建对象有明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。因此出现了各种创建对象的模式。

工厂模式
在一个函数中创建好对象,然后把函数返回。

function createPerson(name,age,job){
    var o=new Object();
    o.name=name;
    o.age=age;
    o.job=job;
    o.sayName=function(){
        alert(this.name);
    };
    return o;
}

var person1=createPerson("Jack",20,"Software Engineer");
var person2=createPerson("Tom",22,"Project Manager");

缺点:没有解决对象识别的问题,即怎么知道一个对象的类型。

构造函数模式

像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。

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

var person1=new Person("Jack",20,"Software Engineer");
var person2=new Person("Tom",22,"Project Manager");

与工厂模式比:

  • 没有显式地创建对象
  • 直接将属性和方法赋予了this对象
  • 没有return语句

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

  1. 创建一个新的空对象
  2. 新对象的_proto_指向构造函数的原型对象
  3. 将构造函数的作用域赋值给新对象
  4. 执行构造函数内部的代码,将属性添加给person中的this对象。
  5. 返回新对象

构造函数与其他函数的唯一区别,就在于调用它们的方式不同。

缺点:构造函数内部的方法会被重复创建,不同实例内的同名函数是不相等的。可通过将方法移到构造函数外部解决这一问题,但面临新问题:封装性不好。

原型模式:

我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。(prototype就是通过调用构造函数而创建的那个对象实例的原型对象)。

使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。

function Person(){
}

Person.prototype.name="Jack";
Person.prototype.age=20;
Person.prototype.job="Software Engineer";
Person.prototype.sayName=function(){
    alert(this.name);
};

var person1=new Person();
person1.sayName();//"Jack"

理解原型对象

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性是一个指向 prototype 属性所在函数的指针。

原型对象的问题:

  1. 它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值,虽然这会在一定程度带来一定的不便,但不是最大的问题,最大的问题是由其共享的本性所决定的。
  2. 对于包含基本值的属性可以通过在实例上添加一个同名属性隐藏原型中的属性。然后,对于包含引用数据类型的值来说,会导致问题。

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

这是创建自定义类型的最常见的方式。

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。所以每个实例都会有自己的一份实例属性的副本,但同时共享着对方法的引用,最大限度的节省了内存。同时支持向构造函数传递参数。

function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.friends=["S","C"];
}

Person.prototype={
    constructor:Person,
    sayName:function(){
        alert(this.name);
    }
};

var person1=new Person(...);

动态原型模式

function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;

    if(typeof this.sayName!="function"){
        Person.prototype.sayName=function(){
            alert(this.name);
        };
    }
}

这里只有sayName()不存在的情况下,才会将它添加到原型中,这段代码只会在初次调用构造函数时才执行。这里对原型所做的修改,能够立刻在所有实例中得到反映。


5.继承

JS实现继承的几种方式

定义一个父类

//定义一个动物类
function Animal(name){
    //属性
    this.name = name || 'Animal';
    //实例方法
    this.sleep = function(){
        console.log(this.name + ' is sleeping~');
    }
}

//原型方法
Animal.prototype.eat = function(food){
    console.log(this.name + ' is eating ' + food);
}

原型链继承
构造函数、实例对象和原型对象的关系:
图片描述

如果使一个原型对象等于另一个对象的实例,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造器函数的指针。假如另一个原型又是另一个类型的实例,如此层层递进,就构成了实例与原型的链条。这就是原型链的基本概念。

function Cat(){
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.eat('fish'));
console.log(cat.sleep());
console.log(cat instanceof Animal); //true 
console.log(cat instanceof Cat); //true

原型链的问题:

  1. 来自原型对象的引用属性被所有实例共享
  2. 创建子类实例时,无法向父类构造函数传递参数

构造函数继承

这种继承方式的基本思想是在子类构造函数的内部调用父类的构造函数。

function Cat(){
    Animal.call(this);
    this.name = name || 'Tom';
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true

构造函数继承的问题:

  1. 实例并不是父类的实例,只是子类的实例
  2. 方法都在构造函数中定义,函数复用无从谈起

组合继承

思路是使用原型链实现对原型属性和方法的继承,借用构造函数实现对实例属性的继承。

function Cat(){
    Animal.call(this);
    this.name = name || 'Tom';
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true

组合继承的问题: 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的同名属性屏蔽了),会多消耗一点内存。

寄生组合式继承

通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点。

function Cat(){
    Animal.call(this);
    this.name = name || 'Tom';
}
(function(){
    //创造一个没有实例方法的类
    var Super = function(){};
    Super.prototype = Animal.prototype;
    Cat.prototype = new Super();
    Cat.prototype.constructor = Cat; 
})();

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true

缺点: 稍显复杂。

拷贝继承

function Cat(){
    var animal = new Animal();
    for(var p in animal){
        Cat.prototype[p] = animal[p];
    }
    Cat.prototype.name = this.name || 'Tom';
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true

拷贝继承存在的问题:

  1. 效率较低,内存占用高(因为要拷贝父类的属性)
  2. 无法获取父类不可枚举的方法(for-in不能访问到不可枚举方法)

6.对象属性的遍历

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还是字符串,也不管是否可枚举。

7.对象的拷贝

对象的拷贝分为浅拷贝和深拷贝,简单来说,浅复制只复制一层对象的属性,而深复制则递归复制了所有层级。深拷贝和浅拷贝最根本的区别在于是否是真正获取了一个对象的复制实体,而不是引用。

浅拷贝的实现

function shallowCopy(obj1) {
  var obj2 = {};
  for (var prop in obj1) {
    if (obj1.hasOwnProperty(prop)) {
      obj2[prop] = obj1[prop];
    }
  }
  return obj2;
}

深拷贝的实现

// 方法一
JSON.parse(JSON.stringify(obj));

// 方法二
function deepCopy(obj1, obj2){
    var obj2 = obj2 || {};
    for(var prop in obj1) {
        if(typeof obj1[prop] === 'object') {
            if(obj1[prop].constructor === Array) {
                obj2[prop] = [];
            } else {
                obj2[prop] = {};
            }
            deepCopy(obj1[prop], obj2[prop]);
        } else {
            obj2[prop] = obj1[prop];
        }
    }
    return obj2;
}

本篇小结是对对象做一个系统的梳理,主要是在秋招之前供自己复习,有错误的地方还请大家指出。


FN归尘
53 声望2 粉丝

on the way~