JavaScript中,创建对象的方式包括两种:对象字面量和使用new表达式。对象字面量是一种灵活方便的书写方式,例如:

 
1
2
3
4
5
6
var o1 = {
    p:”I’m in Object literal”,
    alertP:function(){
        alert(this.p);
    }
}

这样,就用对象字面量创建了一个对象o1,它具有一个成员变量p以及一个成员方法alertP。这种写法不需要定义构造函数,因此不在本文的讨论范围之内。这种写法的缺点是,每创建一个新的对象都需要写出完整的定义语句,不便于创建大量相同类型的对象,不利于使用继承等高级特性。

new表达式是配合构造函数使用的,例如new String(“a string”),调用内置的String函数构造了一个字符串对象。下面我们用构造函数的方式来重新创建一个实现同样功能的对象,首先是定义构造函数,然后是调用new表达式:

1
2
3
4
5
6
7
function CO(){
    this.p = “I’m in constructed object”;
    this.alertP = function(){
        alert(this.p);
    }
}
var o2 = newCO();

那么,在使用new操作符来调用一个构造函数的时候,发生了什么呢?其实很简单,就发生了四件事:

1
2
3
4
var obj  ={};
obj.__proto__ = CO.prototype;
CO.call(obj);
return obj;

第一行,创建一个空对象obj。

第二行,将这个空对象的__proto__成员指向了构造函数对象的prototype成员对象,这是最关键的一步,具体细节将在下文描述。

第三行,将构造函数的作用域赋给新对象,因此CA函数中的this指向新对象obj,然后再调用CO函数。于是我们就给obj对象赋值了一个成员变量p,这个成员变量的值是” I’min constructed object”。

第四行,返回新对象obj。当构造函数里包含返回语句时情况比较特殊,这种情况会在下文中说到。

正确定义JavaScript构造函数

不同于其它的主流编程语言,JavaScript的构造函数并不是作为类的一个特定方法存在的;当任意一个普通函数用于创建一类对象时,它就被称作构造函数,或构造器。一个函数要作为一个真正意义上的构造函数,需要满足下列条件:

1、 在函数内部对新对象(this)的属性进行设置,通常是添加属性和方法。

2、 构造函数可以包含返回语句(不推荐),但返回值必须是this,或者其它非对象类型的值。

上文定义的构造函数CO就是一个标准的、简单的构造函数。下面例子定义的函数C1返回了一个对象,我们可以使用new表达式来调用它,该表达式可以正确返回一个对象:

1
2
3
4
5
6
7
8
function C1(){
    var o = {
        p:”I’m p in C1”
    }
    return o;
}
var o1 = new C1();
alert(o1.p);//I’m p in C1

但这种方式并不是值得推荐的方式,因为对象o1的原型是函数C1内部定义的对象o的原型,也就是Object.prototype。这种方式相当于执行了正常new表达式的前三步,而在第四步的时候返回了C1函数的返回值。该方式同样不便于创建大量相同类型的对象,不利于使用继承等高级特性,并且容易造成混乱,应该摒弃。

一个构造函数在某些情况下完全可以作为普通的功能函数来使用,这是JavaScript灵活性的一个体现。下例定义的C2就是一个“多用途”函数:

1
2
3
4
5
6
7
8
9
10
function C2(a, b){
    this.p = a + b;
    this.alertP = function(){
        alert(this.p);
    }
    return this.p;//此返回语句在C2作为构造函数时没有意义
}
var c2 = new C2(2,3);
c2.alertP();//结果为5
alert(C2(2, 3)); //结果为5

该函数既可以用作构造函数来构造一个对象,也可以作为普通的函数来使用。用作普通函数时,它接收两个参数,并返回两者的相加的结果。为了代码的可读性和可维护性,建议作为构造函数的函数不要掺杂除构造作用以外的代码;同样的,一般的功能函数也不要用作构造对象。

为什么要使用构造函数

根据上文的定义,在表面上看来,构造函数似乎只是对一个新创建的对象进行初始化,增加一些成员变量和方法;然而构造函数的作用远不止这些。为了说明使用构造函数的意义,我们先来回顾一下前文提到的例子。执行var o2 = new CO();创建对象的时候,发生了四件事情:

1
2
3
4
var obj  ={};
obj.__proto__ = CO.prototype;
CO.call(obj);
return obj;

我们说最重要的是第二步,将新生成的对象的__prop__属性赋值为构造函数的prototype属性,使得通过构造函数创建的所有对象可以共享相同的原型。这意味着同一个构造函数创建的所有对象都继承自一个相同的对象,因此它们都是同一个类的对象。关于原型(prototype)和继承的细节,笔者会再另一篇文章中深入说明。

在JavaScript标准中,并没有__prop__这个属性,不过它现在已经是一些主流的JavaScript执行环境默认的一个标准属性,用于指向构造函数的原型。该属性是默认不可见的,而且在各执行环境中实现的细节不尽相同,例如IE浏览器中不存在该属性。我们只要知道JavaScript对象内部存在指向构造函数原型的指针就可以了,这个指针是在调用new表达式的时候自动赋值的,并且我们不应该去修改它。

在构造对象的四个步骤中,我们可以看到,除第二步以外,别的步骤我们无须借助new表达式去实现,因此new表达式不仅仅是对这四个步骤的简化,也是要实现继承的必经之路。

容易混淆的地方

关于JavaScript的构造函数,有一个容易混淆的地方,那就是原型的constructor属性。在JavaScript中,每一个函数都有默认的原型对象属性prototype,该对象默认包含了两个成员属性:constructor和__proto__。关于原型的细节就不在本文赘述了,我们现在关心的是这个constructor属性。

按照面向对象的习惯性思维,我们说构造函数相当于“类”的定义,从而可能会认为constructor属性就是该类实际意义上的构造函数,在new表达式创建一个对象的时候,会直接调用constructor来初始化对象,那就大错特错了。new表达式执行的实际过程已经在上文中介绍过了(四个步骤),其中用于初始化对象的是第三步,调用的初始化函数正是“类函数”本身,而不是constructor。如果没有考虑过这个问题,这一点可能不太好理解,那就让我们举个例子来说明一下吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
function C3(a, b){
    this.p = a + b;
    this.alertP = function(){
        alert(this.p);
    }
}
//我们定义一个函数来覆盖C3原型中的constructor,试图改变属性p的值
function fake(){
    this.p = 100;
}
C3.prototype.constructor = fake; //覆盖C3原型中的constructor
var c3 = new C3(2,3);
c3.alertP();//结果仍然为5

上述代码手动改变了C3原型中的constructor函数,然而却没有对c3对象的创建产生实质的影响,可见在new表达式中,起初始化对象作用的只能是构造函数本身。那么constructor属性的作用是什么呢?一般来说,我们可以使用constructor属性来测试对象的类型:

1
2
var myArray = [1,2,3];
(myArray.constructor == Array); // true

这招对于简单的对象是管用的,涉及到继承或者跨窗口等复杂情况时,可能就没那么灵光了:

1
2
3
4
5
6
7
function f() { this.foo = 1;}
function s() { this.bar = 2; }
s.prototype = new f(); // s继承自f
  
var son = new s(); // 用构造函数s创建一个子类对象
(son.constructor == s); // false
(son.constructor == f); // true

这样的结果可能跟你的预期不相一致,所以使用constructor属性的时候一定要小心,或者干脆不要用它。

js之构造函数的理解的更多相关文章

  1. js原型浅谈理解

    之前在学习原型(prototype)的时候,一直对原型的理解不是很清晰,只是知道每个对象都有一个原型,然后在js中万物又皆对象.在这里谈一下自己对于js原型的简单理解吧. 原型可以实现属性和方法的共享 ...

  2. js高级——构造函数,实例对象和原型对象——prototype、__proto__和constructor构造器

    一.前言 了解JavaScript面向对象,需要先了解三个名词: 构造函数,实例对象和原型对象. 注意:JavaScript中没有类(class)的概念,取而代之的是构造函数,两者类似却又有很大的差别 ...

  3. 对js原型简单的理解和图解

    对js原型简单的理解和图解 最近在努力的学习js中,今天就抽了个空把自己理解的原型,记下一下在笔记中,以后自己查看,有空在会把原型链记录一下. 1.prototype prototype:是一个函数的 ...

  4. 前端【JS】,深入理解原型和原型链

    对于原型和原型链,相信有很多伙伴都说的上来一些,但有具体讲不清楚.但面试的时候又经常会碰到面试官的死亡的追问,我们慢慢来梳理这方面的知识! 要理解原型和原型链的关系,我们首先需要了解几个概念:1.什么 ...

  5. js参数arguments的理解

    原文地址:js参数arguments的理解 对于函数的参数而言,如下例子 function say(name, msg){ alert(name + 'say' + msg); } say('xiao ...

  6. js赋值运算的理解

    简介 js引擎由于为了效率,很多时候的非直接量赋值都不是copy一份在赋值给新的变量,而是一个引用 ps:直接量:直接值数字字符串等 为什么使用len = doms.length; 里的len效率要比 ...

  7. js 模块化的一些理解和es6模块化学习

    模块化 1 IIFE 2 commonjs 3 浏览器中js的模块化 4 简单理解模块加载器的原理  5 es6 之前在参加百度前端技术学院做的小题目的时候,自己写模块的时候 都是写成立即调用表达式( ...

  8. js 时间构造函数

    js 时间构造函数,js中没有类似ToString("yyyy-mm-dd HH:mm:ss") 的方法,但是可以用下面的方式来初始化 var cdate = new Date(& ...

  9. 我对 impress.js 源码的理解

    源码看了两天,删掉了一些优化,和对 ipad 的支持,仅研究了其核心功能的实现,作以下记录. HTML 结构如下: <!doctype html> <html lang=" ...

随机推荐

  1. Python基础学习五

    Python基础学习五 迭代 for x in 变量: 其中变量可以是字符串.列表.字典.集合. 当迭代字典时,通过字典的内置函数value()可以迭代出值:通过字典的内置函数items()可以迭代出 ...

  2. Java常见异常说明汇总

    1. java.lang.nullpointerexception 这个异常大家肯定都经常遇到,异常的解释是"程序遇上了空指针",简单地说就是调用了未经初始化的对象或者是不存在的对 ...

  3. linux下tab作用的描述?

    [Tab] 接在一串指令的第一个字的后面,则为命令补全;     实例怎么描述?什么叫一串指令的第一个字?[Tab] 接在一串指令的第二个字以后时,则为『文件补齐』   实例怎么描述?什么叫一串指令的 ...

  4. SaltStack事件驱动 – event reactor

    Event是SaltStack里面的对每个事件的一个记录,它相比job更加底层,Event能记录更加详细的SaltStack事件,比如Minion服务启动后请求Master签发证书或者证书校验的过程, ...

  5. Linux 创建静态库.a

    gcc -c 只编译不连接 -o *.o(生成.o文件) ar crv  name.a   *.o *.o  (ar 命令把 .o文件打包成  name.a 静态库) 测试 name.a -L 紧跟链 ...

  6. SVN一直清理解决

    svn作为我们经常使用的版本管理服务器,在使用过程中经常需要通过clean up操作来完成本地文件与服务器文件信息及版本信息同步,然而有时会在执行清理命令时提示“清理失败,请执行清理”,并且提示的中文 ...

  7. pycharm2018后版本执行Flask app.run()深坑

    在2018年以前的版本,以上配置在app.run()里面的内置方法

  8. Yii框架的学习指南(策码秀才篇)1-2 一步步学习yii framework

    我也是新手,不过之前学习了tp和ci框架,所以尝鲜想看看yii到底有多强大. 如何新建一个webapp(网站应用)呢,下面是2个步骤: 1. cmd 切换到htdocs下面的yii目录下的framew ...

  9. [LC] 22. Generate Parentheses

    Given n pairs of parentheses, write a function to generate all combinations of well-formed parenthes ...

  10. Spring Boot集成全局唯一ID生成器

    流水号生成器(全局唯一 ID生成器)是服务化系统的基础设施,其在保障系统的正确运行和高可用方面发挥着重要作用.而关于流水号生成算法首屈一指的当属 Snowflake雪花算法,然而 Snowflake本 ...