面向对象编程:我应该使用函数还是对象字面量?

4

我知道一点Java,喜欢整个类和面向对象的编程风格。我正在了解JavaScript,发现它没有像Java和C++那样的“经典”面向对象风格,甚至没有一个简单的构造函数。程序员有很多选择。我做了这个,你觉得怎么样?像这样编写构造函数可以吗?

//constructor of Human
function Human(name, age, size, married) {
    this.n = name;
    this.a = age;
    this.s = size;
    this.m = married
    this.printInformation = function() {
        return "Name: " + this.n + ", Age: " + this.a + ", Size: " + this.s + ", Married: " + this.m;
    };
}

var human1 = new Human("Lenny Linux", 42, "142cm", false);
window.alert(human1.printInformation());

它正在运行,所以我认为应该是可以的。但有一个问题:我还有其他选择。例如,使用这些“对象字面量”或所谓的它们。我可以这样做:

var human1 = {
    name: "Lenny Linux",
    age: 42,
    size: "142cm",
    married: false,
    printInformation: function() {
        //...
    }
}

这个更快是吗?在我写完之后,有一个“人类”的对象需要初始化,而另一个则不需要。但我更喜欢另一个。因为它更容易让我忽略我的代码。但第一个例子并不像典型的JavaScript那样,对吧?所以我必须忘记关于Java的一切,使用特殊的JavaScript样式?还是我可以保持我的Java代码风格?你认为呢?

PS:对于第一个例子,还有一个问题困扰着我。我无法强制程序员在初始化构造函数时只使用布尔值,如果他在“married”变量上。但我真的想强迫他这样做。年龄也是如此:只能是整数……有什么好的想法吗?

谢谢!


1
我认为这不足以作为一个答案,但一个重要的区别是,如果Human1Human2像这样声明:function Human1(name) { this.name = name; } function Human2(name) { return { name: name }; },那么new Human1("John Doe") instanceof Human1将是true,但new Human2("John Doe") instanceof Human2将是false - icktoofay
你会发现自己更经常使用对象字面量而不是伪经典继承。它们对于大多数工作来说更加方便,不会创建太多开销。如果您需要以更具功能性的方式创建对象,则可以查看missingno发布的内容。正如Crockford所说:“伪经典模式旨在看起来有点面向对象,但它看起来非常陌生” :) 顺便说一句,这是一本非常棒的书:http://www.oreilly.de/catalog/9780596517748/ - evildead
@icktoofay:instanceof 会促进不良编码习惯,而且并不是非常有用(它很容易混淆,并且有很多情况下它甚至都不适用)。你通常最好只是接受鸭子类型,并让 Human1 和 Human2 可互换。 - hugomg
2个回答

5
你所描述的两种方法实际上是完全等价的(除了语法),从Javascript角度来看,大多数情况下都可以使用。话虽如此,你不应该仅仅基于目前的舒适程度来选择做什么 - 长期来看,你需要习惯与语言一起工作而不是对抗它。接着...

如果我想像Java一样强制字段成为某种类型怎么办?

Javascript是动态类型的,因此您将很难尝试应用相同的静态类型范例。您可以尝试在对象构造函数期间进行运行时检查(使用typeof),但通常这并不值得麻烦,因为检查仍然是在运行时进行的,不检查可能会导致类似的错误,而typeof非常有限(检查数组是否是一个棘手的问题,检查接口非常麻烦,让我们不要开始谈论“外星”浏览器对象...)

最后,不要过于担心动态类型 - 您很快就会习惯它。

如果你说对象字面量方法和构造函数方法返回相同的结果,那么区别在哪里?

首先,虽然对象字面量是一种非常简洁的语法,但有些东西需要拆分成多个语句,因此您需要一个函数来完成它们:

//Note: lowercase name since I won't be using 'new here...
//there is a good convention for only using capital names on 
// "real" constructors
function create_human(name, age){
    var obj = {};
    obj.name = name;
    obj.age = age;

    //this needs to be on a separate statement
    //since it involves the other fields
    obj.isAdult = (obj.age >= 21);

    return obj;
}

//not using 'new ...yet
var that_penguin = create_human("Lenny", 42);

请注意,对象字面量在这里仍然非常有用,并且非常流行使用它们来提供命名和默认参数,以解决通常具有大型参数列表的情况:
function  create_human(args){
    var obj;
    obj.name = args.name;
    //...
}

var x  = create_human({
    name: 'Lenny',
    age: 42,
    //...
});

记住:到目前为止,使用函数构建对象与对象字面量仅是样式和组织问题,最好的方法通常取决于您处理的特定情况。根据我的经验,对象字面量非常适合创建单例和配置字典,而函数非常有用,可以强制执行复杂对象中的不变量或提供常见对象的速记。
那么,“真正的”构造函数、new和this是怎么回事呢?
手动显式构建对象的一个缺点是我们错过了一些我们习惯于的面向对象优点。通过为每个对象提供其方法的副本,我们不仅浪费空间(在经典语言中,这些将被存储在类中),而且我们失去了差异化继承(因为一切都是静态的)。JavaScript 处理此问题的方式是使用原型。所有对象都有一个原型,在查找属性(或方法)时,如果没有立即找到,它会在原型中递归搜索。
原型的常见用例是使对象类保留其实例变量,但共享方法:
lenny:
    name: "Lenny"
    age: 42
    __proto__: Person.prototype

glenda:
    name: "Glenda"
    age: 19
    __proto__: Person.prototype

Person.prototype:
    printInformation: ...
    tons of methods: ...

这样我们可以访问lenny.printInformation方法,而不会注意到这个方法被与Glenda共享。
要创建一个具有原型的对象,您可以使用Object.create(至少在较新的浏览器中),或者使用构造函数和new运算符的旧方式:
function Person(name, age){
    //The 'new operator provides an empty
    // 'this' object with a suitable prototype.
    // The constructor function just needs to fill in the 
    // instance variables.

    this.name = name;
    this.age = age;

    //note: no return statement!

    //and no methods as well
    //(unless they need to be closures but thats another thing)...
}

//Methods in Person.prototype, will be shared by all Person instances:
Person.prototype = {
    printInformation: function(){
        console.log('my age is', this.age);
    }
};

var lenny = new Person("Lenny", 42);

总结

如果您想使用语言的原型功能,请使用构造函数和new运算符。

否则,请使用普通函数或对象字面量。


我不太确定。在伪经典风格中是否可能隐藏数据,还是只有在函数闭包风格的对象创建中才可能? - evildead
@evildead:是的,闭包在某种程度上是拥有私有变量的唯一方法(除了函数绑定的把戏)。然而,闭合变量是静态的,无法被在闭合函数之外创建的方法访问。为了弥补这一点,大多数人要么只是使用_fake_private_variables,要么做像寄生继承这样的事情。我最初没有提到这些内容,因为我觉得与原始问题无关。 - hugomg

1
你应该从这本中了解原型继承。
    //constructor of Human
    function Human(name, age, size, married) {
        this.n = name;
        this.a = age;
        this.s = size;
        this.m = married;
        this.printInformation = function() {
            return "Name: " + this.n + ", Age: " + this.a + ", Size: " + this.s +
                 ", Married: " + this.m;
        };
    }

    var human1 = new Human("Lenny Linux", 42, "142cm", false);
    window.alert(human1.printInformation());

所以,printInformation 是将其放在 Human 的原型上创建共享方法的好选择。当你定义 Human 时,所有对 this 的引用都将被设置为一个空对象字面量。同时,在幕后,一个隐藏的 _proto_ 属性被分配给实际上是指向 Human.prototype 的对象引用的对象。因此,只要不将 Human.prototype 引用新对象,那么所有 Human 实例将具有指向 Human.protoype 的相同函数指针。因此,这是你想要的效率标准示例。
    //constructor of Human
    function Human(name, age, size, married) {
        this.n = name;
        this.a = age;
        this.s = size;
        this.m = married;
    };
    Human.prototype.printInformation = function() {
        return "Name: " + this.n + ", Age: " + this.a + ", Size: " + this.s +
            ", Married: " + this.m;
    };
    var human1 = new Human("Lenny Linux", 42, "142cm", false);
    window.alert(human1.printInformation());

在运行时,解释器将尝试在实例本身上查找printInformation,但在这种情况下找不到。因此,它将遵循_proto_链接。它在那里找到了它,因为Human.prototype.printInformation是一个函数。

这样做的原因是高效的,因为所有实例都指向同一个对象:Human.prototype;而当您在构造函数内部说this.method = function() {}时,每个实例都会被分配一个新的函数。


@Ryan - 嗯,不确定你在这里做什么,但这不是达成目的的方式。只需编写答案并发布即可。 - Jared Farrish
你在开玩笑吧...这是一个很长的解释 :P - Ryan
1
在SO中没有“占位符答案”。你的是唯一的答案;只需编写并发布即可。为什么要为不存在的答案请求点赞?(顺便说一句,我没有给它投反对票。) - Jared Farrish
1
“references to this will be an object literal” 的意思并不是一个对象字面量。对象字面量是一种语法,可用于创建对象,而构造函数并不需要该语法。 - user113716
即使在对象文字中,如果您将一个函数定义为对象的属性,并且您使用 obj.method()var method = obj.method; method.call(obj),则在执行 obj.method 期间,this 将引用 obj - Ryan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接