黄子涵

5.7 对象的生成

5.7.1 对象字面量

在 JavaScript 程序中,如果要使用对象,就需要首先生成该对象。其中一种方法是通过对象字面量来实现对象的生成。

下面列举了一些可以使用对象字面量的情况。请注意这里并没有作严格的分类。

  • 作为 singleton 模式的用法。
  • 作为多值数据的用法(函数的参数或返回值等)。
  • 用于替代构造函数来生成对象。

作为 singleton 模式的用法

在设计模式中有一种 singleton 模式。在基于类的开发过程中,这种模式可以将类的实例数限定为 1 个。

JavaScript 可以实现基于类的程序设计,不过通常会作如下约定:若只需一个对象实例,则不会去设计一个类,而是会使用对象字面量。对类(构造函数)进行设计以实现 singleton 模式的想法完全是一种基于类的思考方式,在 JavaScript 中我们只需直接使用对象字面量即可。

作为多值数据的用法

可以通过对象字面量来实现多值数据。这种用法与作为关联数组的对象是相通的。例如,在代码清单 5.3 中有一个需要三个参数的函数,对参数是否为数值型的判断已被省略。

代码清单 5.3 接受多个参数的函数
function hzh(x, y, z) {
    return Math.sqrt(x * x + y * y + z * z);
}

console.log("调用hzh函数:");
console.log(hzh(3, 2, 2));
[Running] node "e:\HMV\JavaScript\JavaScript.js"
调用hzh函数:
4.123105625617661

[Done] exited with code=0 in 2.023 seconds
代码清单 5.4 接受对象的函数
function hzh(pos) {
    return Math.sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z);
}

console.log("调用hzh函数:");
console.log(hzh({x:3, y:2, z:2}));
[Running] node "e:\HMV\JavaScript\JavaScript.js"
调用hzh函数:
4.123105625617661

[Done] exited with code=0 in 0.233 seconds

很难说哪一种方法更好,两者各有千秋。参数的数量为 3 个的情况有些微妙,或许认为代码清单 5.3 中的方法更为简单的读者会更多一些。

不过,当参数的数量越来越多时,代码清单 5.4 中的方法的优势就会体现出来。如果用代码清单 5.3 中的方法,参数数量增加之后,弄错实参的排列顺序的可能性也会上升,而 JavaScript 这样的动态程序设计语言对参数类型的检测很弱。如果像代码清单 5.4 这样使用对象作为参数,实参以对象字面量的方式传递,就不需要考虑排列的顺序,只需要使用名称即可。在其他一些程序设计语言中,支持对参数进行命名的功能,这种功能也具有类似的优点。

在 JavaScript 中,有一种模拟出默认参数的效果的习惯用法(代码清单 5.5)。这种方法需要与使用对象作为参数的方式结合使用才能发挥效果。所谓默认参数,是指在调用函数时如果没有实参,或是传递了null,则会传递一个指定的值。JavaScript 并不支持默认参数这一功能,但可以通过代码清单 5.5 这样的形式来实现。

通过 || 运算可以将参数作为布尔型来判断真假,其中利用了若调用函数时没有实参参数的值则为undefined 这一特性。通常来说,在函数内对参数进行赋值不是一种好习惯(不仅是 JavaScript,所有的程序语言都是如此),不过下面的做法被当作了一种习惯用法。

代码清单 5.5 模拟了默认参数的效果的习惯用法
function hzh(pos) {
    pos = pos || { x:0, y:0, z:0 }; // 如果没有收到参数pos的话,则使用默认值
    return Math.sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z);
}

console.log("调用hzh函数:");
console.log(hzh({x:3, y:2, z:2}));
function hzh(pos) {
    pos = pos || { x:0, y:0, z:0 }; // 如果没有收到参数pos的话,则使用默认值
    return Math.sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z);
}

console.log("调用hzh函数:");
console.log(hzh({x:3, y:2, z:2}));
代码清单 5.6 返回多值数据的函数
function hzh(pos) {
    // 省略
    return { x:3, y:2, z:2};
}

var pos = hzh();

console.log("输出返回值:");
console.log(pos.x, pos.y, pos.z);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
调用hzh函数:
3 2 2

[Done] exited with code=0 in 0.191 seconds

用于替代构造函数来生成对象

最后我们介绍一下通过对象字面量来实现一个用于替代构造函数的函数的用法。该函数的功能是生成一个对象,所以需要以对象字面量作为返回值,从形式上来说,它和返回多值数据的函数是相同的。根据狭义的面向对象的定义,多值数据与对象的区别仅在于是否具有特定的执行方式。

和代码清单 5.6 一样,直接在代码中书写数值没有什么意义,这里仅仅是作为一个例子用于说明而已(代码清单 5.7)。

代码清单 5.7 用于生成对象的函数(还有改进的余地)
function hzh() {
    return { x:3, y:2, z:2,
        huangzihan: function() {
            return Math.sqrt(this.x * this.x + 
                this.y * this.y + this.z * this.z);
        }
    };
}

var obj = hzh();
console.log("调用obj对象的方法:");
console.log(obj.huangzihan());
[Running] node "e:\HMV\JavaScript\JavaScript.js"
调用obj对象的方法:
4.123105625617661

[Done] exited with code=0 in 0.26 seconds

使用返回对象字面量的函数,与通过 new 表达式来调用构造函数,是两种不同风格的生成对象的手段。

专栏

JavaScript中用于函数返回多个值的增强功能

通过 JavaScript 的增强功能,可以像下面这样,通过数组实现将返回值逐个返回的功能。

function hzh() {
    return [1,9,1,2,4,8,9,6,0,1,7];
}

var a,b,c,d,e,f,g,h,i,j,k;
[a,b,c,d,e,f,g,h,i,j,k] = hzh();
console.log("输出数组返回值:");
console.log(a,b,c,d,e,f,g,h,i,j,k);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
输出数组返回值:
1 9 1 2 4 8 9 6 0 1 7

[Done] exited with code=0 in 0.284 seconds

5.7.2 构造函数与 new 表达式

构造函数是用于生成对象的函数。之后会再详述函数与构造函数的区别,这里首先介绍一个具体例子(代码清单 5.8)。

可以直观地将代码清单 5.8 理解为 MyClass 类的类定义。在调用时通过 new 来生成一个对象实例。

代码清单 5.8 构造函数的例子
function MyClass(x, y) {
    this.x = x;
    this.y = y;
}

// 对构造函数的调用
var obj = new MyClass(3, 2);
console.log("obj.x = " + obj.x); 
console.log("obj.y = " + obj.y); 
[Running] node "e:\HMV\JavaScript\JavaScript.js"
obj.x = 3
obj.y = 2

[Done] exited with code=0 in 0.173 seconds

从形式上来看,构造函数的调用方式如下。

  • 构造函数本身和普通的函数声明形式相同。
  • 构造函数通过 new 表达式来调用。
  • 调用构造函数的 new 表达式的值是(被新生成的)对象的引用。
  • 通过 new 表达式调用的构造函数内的 this 引用引用了(被新生成的)对象。
new 表达式的操作

在此说明一下 new 表达式在求值时的操作。首先生成一个不具有特别的操作对象。之后通过 new 表达式调用指定的函数(即构造函数)。构造函数内的 this 引用引用了新生成的对象。执行完构造函数后,它将返回对象的引用作为 new 表达式的值。

图 5.5 构造函数的操作图

image

构造函数调用

构造函数总是由 new 表达式调用。为了与通常的函数调用相区别,将使用 new 表达式的调用,称为构造函数调用。构造函数与通常的函数的区别在于调用方式不同。任何函数都可以通过 new 表达式调用,因此,所有的函数都可以作为构造函数。也就是说,如果一个函数通过函数调用的方式使用,则是一个函数;如果通过构造函数调用的方式使用,则是一个构造函数。在实际开发中,通常会分别设计用于函数调用的函数与用于构造函数调用的函数,所以方便起见,将为了构造函数调用而设计的函数称为构造函数。构造函数的名称一般以大写字母开始(如 MyClass)。

构造函数在最后会隐式地执行 return this 操作。那么,如果在构造函数中显式地写有 return 语句,会发生什么情况呢?结果可能不容易理解。通过 return 返回一个对象之后,它将成为调用构造函数的 new 表达式的值。也就是说,使用 new 表达式后返回的,可能是所生成的对象以外的其他对象。然而,如果调用的构造函数中的 return 返回的是基本类型的值,则会无视这一返回值,仍然隐式地执行 return this 操作。

这种操作常常会造成混乱,我们建议不要再在构造函数内使用 return 语句。

5.7.3 构造函数与类的定义

通过 new 表达式调用普通的函数并生成一个对象,是一种不容易理解的语言特性。不过,这已经满足了类定义所必需的功能。

代码清单 5.9 是一个实现了定义一个具有域与方法的类的构造函数的例子。

代码清单 5.9 模拟类定义(尚有改进的余地)
// 相当于类的定义
function Huangzihan(x, y) {
    // 相当于域
    this.x = x;
    this.y = y;
    // 相当于方法
    this.show = function() {
        console.log(this.x, this.y);
    }
}

// 对构造函数的调用(实例生成)
var hzh = new Huangzihan(3, 2);
console.log("访问obj对象的show方法:");
console.log(hzh.show());
[Running] node "e:\HMV\JavaScript\JavaScript.js"
访问obj对象的show方法:
3 2
undefined

[Done] exited with code=0 in 0.329 seconds

只要按照代码清单 5.9,就能够从形式上实现 JavaScript 的类定义。不过,代码清单 5.9 作为类的定义还存在以下两个问题。前者可以通过原型继承来解决,而后者可以通过闭包来解决

  • 由于所有的实例都是复制了同一个方法所定义的实体,所以效率(内存效率与执行效率)低下。
  • 无法对属性值进行访问控制(private 或 public 等)。
posted @ 2022-05-28 09:37  黄子涵  阅读(29)  评论(0编辑  收藏  举报