js中对象的创建及理解

初学 JavalScrip 时,它的自由让人很不适应,特别是对于对象的创建,很是疑惑:它没有类的概念(初学时了解的水平),也没有构造函数, 而更不能理解的是,它的实例是通过 new 函数得到的。直到看了《JavaScrip高级程序设计》之后,才恍然大悟,特此总结,希望能给那些初学者一点解惑之光,若有不足之处,还请斧正。

用 C# 创建对象

首先,我们看一下,在 C# 中怎么创建一个对象

  • 定义类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 类的定义
    public class Person
    {
    // 定义属性
    public String Name {get;set;}
    public int Age {get;set;}

    // 构造函数
    public Person(string name,int age)
    {
    this.Name = name;
    this.Age = age;
    }
    }
  • 创建C#对象

    1
    Person person = new Person('zhangsan',18);

    上面就是 C# 创建对象的过程

用 js 创建对象

现在我们再来看一下 js 中如何创建对象(ES5中构造函数方式)

  • 定义函数

    1
    2
    3
    4
    5
    fuction Person(name,age)
    {
    this.name = name;
    this.age = age;
    }
  • 创建 js 对象

    1
    2
    // const ES6 中的声明
    const person = new Person('lisi',19);

js 创建对象原理分析

要理解 js 中创建对象为什么可以 new 函数得到,我们需要追本溯源,一步一步了解 js 中创建对象的演变

  • 最开始是 new Object 形式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var person = new Object();
    person.name = 'lisi';
    person.age = 19

    // 或者
    var person = {
    name: 'lisi',
    age: 19
    }
  • 然后是工厂模式

    1
    2
    3
    4
    5
    6
    function createPerson(name, age){
    var o = new Object();
    o.name = name;
    o.age = age;
    return o;
    }
  • 随着演变,出现了构造函数模式

    1
    2
    3
    4
    5
    // 定义构造函数时,首字母建议大写,用于区别一般函数
    function Person(name, age){
    this.name = name;
    this.age = age;
    }
  • 分析

    对比工厂模式和构造函数模式,我们可以看到有如下区别

    • 后者没有显式地创建对象

    • 直接将属性和方法赋给了 this 对象

    • 没有 return 语句

    • 创建新实例的时候,必须用 new

    构造函数在创建对象的时候,其实经过了下面步骤:

    • 创建一个新对象
    • 将构造函数的作用域赋给新对象(因此 this 指向了新的对象)
    • 执行构造函数的代码(为这个新对象添加属性)
    • 返回新对象

通过上面分析,我们就很容易地理解了,为什么 new 一个函数可以创建对象

将构造函数当作函数

构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过 new 操作符来调用,那它跟普通函数也不会有什么两样。不过直接调用的时候,它就是向上下文的 this 添加字段。

构造函数方式的问题

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

例如有如下构造函数

1
2
3
4
5
6
7
8
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}

当实例化多个 Person 后,每个实例中的 sayName 不是同一个,上面的定义可以等价为:

1
2
3
4
5
6
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("alert(this.name)"); // 与声明函数在逻辑上是等价的
}

然而,创建两个完成同样任务的 Function 实例的确没有必要;况且有 this 对象在,根本不用在执行代码前就把函数绑定到特定对象上面。因此,大可像下面这样,通过把函数定义转移到构造函数外部来解决这个问题。

1
2
3
4
5
6
7
8
9
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}

我们把 sayName()函数的定义转移到了构造函数外部。而在构造函数内部,我们将 sayName 属性设置成等于全局的 sayName 函数。这样一来,由于 sayName 包含的是一个指向函数的指针。

最优的对象声明方式

既然分析了 js 创建对象的原理,那么现在就必须要分享一下,什么是最优的对象声明方式了。原理部分直接省略,此处只给出最终的答案——动态原型模式:

1
2
3
4
5
6
7
8
9
10
11
12
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);
};
}
}