JavaScript对象的创建方式

在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)的更多相关文章

  1. 咱们来聊聊JS中的异步,以及如何异步,菜鸟版

    为什么需要异步?why?来看一段代码. 问题1: for(var i=0;i<100000;i++){ } alert('hello world!!!'); 这段代码的意思是执行100...次后 ...

  2. 【原】聊聊js代码异常监控

    在平时的工作,js报错是比较常见的一个情景,尤其是有一些错误可能我们在本地测试的时候测试不出来,当发布到线上之后才可以发现,如果抢救及时,那还好,假如很晚才发 现,那就可能造成很大的损失了.如果我们前 ...

  3. 聊聊js中的typeof

    内容: 1.typeof 2.值类型和引用类型 3.强制类型转换 typeof 官方文档:typeof 1.作用: 操作符返回一个字符串,指示未经计算的操作数的类型. 2.语法: typeof ope ...

  4. 聊聊JS与设计模式之(工厂Factory)篇------(麦当劳的故事)

    一,总体概要 1,笔者浅谈 说起设计模式其实并不是什么很新奇的概念,它也不是基于特定语言所形成的产物,它是基于软件设计原则以及相关的方法论和经过特定时期衍生出的若干解决方案.本文会以一个实例带入大家学 ...

  5. 聊聊js运算符 ‘与(&&)’和‘ 或(||)’

    一,先来几个问题,看给位能都全部答对. var objA1 = {x:1}; var objA2 = {x:2}; var resultA = objA1 && objA2; //请问 ...

  6. 聊聊js跨域

    推荐先读一下这篇文章: https://segmentfault.com/a/1190000012469713http://www.dailichun.com/2017/03/22/ajaxCross ...

  7. 聊聊JS动画库:Velocity.js

    前言 又到了炎热的7月,很久没有更新技术文章了,原因是上月月底实习结束,从公司离职.然后最近在弄自己的项目和考驾照,为了下次公司的应聘做准备,送别了女朋友到外地,哩哩啦啦半个月把一切事情都办妥后,还是 ...

  8. 聊聊JS的二进制家族:Blob、ArrayBuffer和Buffer

    事实上,前端很少涉及对二进制数据的处理,但即便如此,我们偶尔总能在角落里看见它们的身影. 今天我们就来聊一聊前端的二进制家族:Blob.ArrayBuffer和Buffer 概述 Blob: 前端的一 ...

  9. 前端监控系列2 |聊聊 JS 错误监控那些事儿

    作者:彭莉,火山引擎 APM 研发工程师.2020年加入字节,负责前端监控 SDK 的开发维护.平台数据消费的探索和落地. 有必要针对 JS 错误做监控吗? 我们可以先假设不对 JS 错误做监控,试想 ...

随机推荐

  1. weui中的日期选择控件关于时间段的设置!

    近日用到了日期控件,但是需要把时和分去掉,功能上是做一个预约的功能,所以只需要在年月日后面提供时间段的选择. BUG在于如果第一次点开弹框而没做任何操作,然后点了其他任意区域则弹框关闭,甚至你的8:0 ...

  2. JMeter基本元素简介

    JMeter基本元素简介 一.测试计划 测试计划对象有一个复选框叫做"Functional Testing"(函数测试模式),如果选中,它将使jmeter记录服务端返回的每一个样例 ...

  3. 关于python 2.7要求输出汉字问题

    对于python 2.7,使用print u'好好学习'会出现异常.可用以下作为汉字输出,获取数据(字符串)时,用unicode(str, "utf8")生成unicode对象 p ...

  4. centos7 hue安装

    p.MsoNormal { margin: 0pt; margin-bottom: .0001pt; text-align: justify; font-family: Calibri; font-s ...

  5. github设置添加SSH

    很多朋友在用github管理项目的时候,都是直接使用https url克隆到本地,当然也有有些人使用 SSH url 克隆到本地.然而,为什么绝大多数人会使用https url克隆呢? 这是因为,使用 ...

  6. vue2.0项目中使用Ueditor富文本编辑器示例

    最近在vue项目中需要使用富文本编辑器,于是将Ueditor集成进来,作为公共组件. 在线预览:https://suweiteng.github.io/vue2-management-platform ...

  7. html dl dt dd标签元素语法结构与使用

    dl dt dd认识及dl dt dd使用方法 标签用于定义列表类型标签. dl dt dd目录 dl dt dd介绍 结构语法 dl dt dd案例 dl dt dd总结 一.dl dt dd认识 ...

  8. 二叉搜索树的平衡--AVL树和树的旋转(图解)

    二叉搜索树只有保持平衡时其查找效率才会高. 要保持二叉搜索树的平衡不是一件易事.不过还是有一些非常经典的办法可以做到,其中最好的方法就是将二叉搜索树实现为AVL树. AVL树得名于它的发明者 G.M. ...

  9. IronFort---基于Django和Websocket的堡垒机

    WebSSH有很多,基于Django的Web服务也有很多,使用Paramiko在Python中进行SSH访问的就更多了.但是通过gevent将三者结合起来,实现通过浏览器访问的堡垒机就很少见了.本文将 ...

  10. 详细介绍关联规则Apriori算法及实现

    看了很多博客,关于关联规则的介绍想做一个详细的汇总:  一.概念                                                                     ...