JavaScript: 我需要为对象中的每一个变量都加上 this.var 吗?

40
在C++中,这是我最熟悉的语言,通常声明一个对象的方式如下:
class foo
{
public:
    int bar;
    int getBar() { return bar; }
}

调用 getBar() 没有问题(忽略了可能未初始化的 bar)。在 getBar() 中,变量 bar 处于类 foo 的范围内,所以除非我真的需要明确指出我正在引用该类的 bar 而不是参数,否则我不需要写 this->bar

现在,我试图在JavaScript中开始使用OOP。因此,我查找如何定义类并尝试相同的方法:

function foo()
{
     this.bar = 0;
     this.getBar = function() { return bar; }
}
而且它报错:bar 未定义。将 bar 改为 this.bar 可以解决问题,但是对于每个变量都这样做会使我的代码很混乱。是否每个变量都需要这样做?由于找不到任何相关的问题,这让我觉得我在做一些根本性错误的事情。
编辑:从评论中得知,this.bar,一个对象的属性,引用了与局部变量 bar 不同的东西。可以有其他方法来定义对象,在这种情况下不需要这样做吗?请问涉及作用域和对象方面的原因是什么?

3
.bar 是一个对象属性,而不是变量。因此,如果要从对象中获取该属性,您需要通过对该对象的某些引用来实现。如果 this 引用您的对象,则 this.bar 将为您提供该属性。 - I Hate Lazy
3
this.barbar 不是完全相同的。不过你可以使用 var bar = this.bar,这样 bar 就会引用 this.bar - Loktar
@Loktar 只有当 this.bar 是一个对象或数组时才这样做... - Alnitak
3
为了让OP清楚明白,它将获得其当前值的副本。 - I Hate Lazy
@user1689607,如果它是一个原始类型,它将获得其当前值的副本;如果它是一个对象或数组,它将获得对该对象或数组的“引用副本”。 - Alnitak
关于您在编辑中明确提出的更深层次的问题,请参阅为什么JavaScript中没有隐式的this - Bergi
6个回答

36

JavaScript没有基于类的对象模型,而是使用更为强大的原型继承。虽然可以模拟类,但并不适合。在JavaScript中,一切都是对象,并且对象可以从其他对象继承。

构造函数只是一个给新创建的对象分配属性的函数。通过使用new关键字调用构造函数创建的对象可以通过this关键字引用(它是局部函数)。

方法只是一个在对象上调用的函数,再次使用成员运算符(点、括号)作为该对象的属性,使用this指向该对象。这对新手造成了很多困惑,因为如果您将该函数传递(例如到事件监听器),则它将“脱离”访问它的对象。

那么继承在哪里呢?“类”的实例都从同一个原型对象继承。方法被定义为该对象上的函数属性(而不是每个实例上的一个函数),调用这些方法的实例只是继承该属性。

示例:

function Foo() {
    this.bar = "foo"; // creating a property on the instance
}
Foo.prototype.foo = 0; // of course you also can define other values to inherit
Foo.prototype.getBar = function() {
    // quite useless
    return this.bar;
}

var foo = new Foo; // creates an object which inherits from Foo.prototype,
                   // applies the Foo constructor on it and assigns it to the var
foo.getBar(); // "foo" - the inherited function is applied on the object and
              // returns its "bar" property
foo.bar; // "foo" - we could have done this easier.
foo[foo.bar]; // 0 - access the "foo" property, which is inherited
foo.foo = 1;  // and now overwrite it by creating an own property of foo
foo[foo.getBar()]; // 1 - gets the overwritten property value. Notice that
(new Foo).foo;     // is still 0

所以,我们只使用了该对象的属性,并对其感到满意。但它们都是“公共的”,可以被覆盖/更改/删除!如果这并不重要,那么你很幸运。您可以通过在名称前加下划线来指示属性的“私有性”,但这只是向其他开发人员提供的提示,可能不会被遵守(特别是在出现错误时)。

因此,聪明的头脑找到了一种解决方案,使用构造函数作为闭包,允许创建私有“属性”。每次执行JavaScript函数时,都会为局部变量创建一个新的变量环境,一旦执行完成,这些变量可能会被垃圾回收。在该范围内声明的每个函数也都可以访问这些变量,只要这些函数可以被调用(例如通过事件侦听器),环境就必须保持存在。因此,通过从构造函数中导出本地定义的函数,您可以保留具有仅可通过这些函数访问的本地变量的变量环境。

让我们看看实际应用:

function Foo() {
    var bar = "foo"; // a local variable
    this.getBar = function getter() {
        return bar; // accesses the local variable
    }; // the assignment to a property makes it available to outside
}

var foo = new Foo; // an object with one method, inheriting from a [currently] empty prototype
foo.getBar(); // "foo" - receives us the value of the "bar" variable in the constructor

这个getter函数被定义在构造函数内部,现在被称为“privileged方法”,因为它可以访问“private”(局部)的“attributes”(变量)。bar的值将永远不会改变。当然,您也可以声明一个setter函数,并在其中添加一些验证等操作。

请注意,原型对象上的方法无法访问构造函数的局部变量,但它们可以使用特权方法。我们来添加一个:

Foo.prototype.getFooBar = function() {
    return this.getBar() + "bar"; // access the "getBar" function on "this" instance
}
// the inheritance is dynamic, so we can use it on our existing foo object
foo.getFooBar(); // "foobar" - concatenated the "bar" value with a custom suffix

因此,您可以结合这两种方法。请注意,特权方法需要更多的内存,因为您会创建不同作用域链但代码相同的不同函数对象。如果您将要创建大量实例,应该只在原型上定义方法。

当您从一个“类”继承到另一个时,情况甚至变得更加复杂 - 基本上您必须使子原型对象继承自父原型对象,并在子实例上应用父构造函数来创建“私有属性”。请参阅正确的JavaScript继承继承原型中的私有变量在JavaScript模块模式中定义私有字段成员和继承如何在JS揭示原型模式中实现继承?


这对我理解这里到底发生了什么非常有帮助。实际上,最初的动机只是因为我厌烦必须在每个东西前面加上“this”,但是真正理解为什么我必须这样做是很好的。 - Chaosed0
ECMAScript中没有“成员运算符”。正确的术语是“属性访问器表示法”。 - PointedEars
@PointedEars:当然,那是官方术语。但是“成员运算符”(似乎是由MDN创造的)真的是错误或不具描述性吗?当然是数学意义上的运算符,而不是二进制ES运算符(§11.5-11.11)。 - Bergi
1
这个误称可能源自David Flanagan的《JavaScript权威指南》(远非“权威”),他实际上将[]列为“数组运算符”。是的,那是错误的。考虑可能的“操作数”。而且对于[],它的运算符优先级是什么?我刚刚编辑了那篇明显错误的MDN文章,并建议将其移出“运算符”命名空间。您应该更新您的链接并在一段时间后重新检查它。 - PointedEars
array 运算符是“明显”的错误,是的 :-) 运算符优先级 对我来说很有意义,将其赋予最高可能的优先级(在分组运算符之后)。尽管如此,非常感谢您改进 MDN :-) 请注意,运算符页面 也列出了 get/setletthis 甚至 function 作为运算符 :-/ - Bergi
如果它是一个运算符,那么在执行操作之前将必须解析操作数。这里应用了数学:一个操作具有一个或多个特定类型的操作数,操作的结果也是特定类型的(有时取决于操作数的类型)。至少对于点属性访问器,这些都不适用(这可能是为什么Flanagan认为[]是一个运算符),而后者不适用于括号属性访问器。关于MDN:不用谢,显然还有很多工作要做。 - PointedEars

7
明确地说,this.foo(正如您所理解的那样)意味着您对当前由this引用的对象的属性foo感兴趣。因此,如果您使用:this.foo ='bar';,则将使this所引用的当前对象的属性foo等于bar
在JavaScript中,this关键字并不总是像C ++中那样意义相同。这里我可以给您一个例子:
function Person(name) {
   this.name = name;
   console.log(this); //Developer {language: "js", name: "foo"} if called by Developer
}

function Developer(name, language) {
   this.language = language;
   Person.call(this, name);
}

var dev = new Developer('foo', 'js');

在上面的例子中,我们使用函数Developer作为上下文调用函数Person,因此this指向将由Developer创建的对象。从console.log结果可以看出,this来自于Developer。通过方法call的第一个参数,我们指定了函数调用时的上下文。
如果您不使用this,那么您创建的属性将成为局部变量。正如您可能已经知道的,JavaScript具有功能范围,因此该变量将是局部变量,仅在声明它的函数(以及当然所有在父级内声明的子函数)中可见。以下是一个例子:
function foo() {
    var bar = 'foobar';
    this.getBar = function () {
        return bar;
    }
}

var f = new foo();
console.log(f.getBar());  //'foobar'

当您使用var关键字时,这是正确的。这意味着如果您忘记var,则将bar定义为局部变量,不幸的是bar将变为全局。

function foo() {
    bar = 'foobar';
    this.getBar = function () {
        return bar;
    }
}

var f = new foo();
console.log(window.bar);  //'foobar'

局部作用域可以帮助你实现面向对象编程(OOP)最大的优点之一——隐私和封装。

实际应用示例:

function ShoppingCart() {
    var items = [];

    this.getPrice = function () {
       var total = 0;
       for (var i = 0; i < items.length; i += 1) {
          total += items[i].price;
       }
       return total;
    }

    this.addItem = function (item) {
        items.push(item);
    }

    this.checkOut = function () {
        var serializedItems = JSON.strigify(items);
        //send request to the server...
    }
}

var cart = new ShoppingCart();
cart.addItem({ price: 10, type: 'T-shirt' });
cart.addItem({ price: 20, type: 'Pants' });
console.log(cart.getPrice()); //30

JavaScript作用域的又一个好处就是可以应用模块模式,在模块模式中,你可以通过JavaScript的本地函数作用域来模拟隐私,这种方法可以同时拥有私有属性和方法。以下是一个例子:

var module = (function {

    var privateProperty = 42;

    function privateMethod() {
        console.log('I\'m private');
    }
    return {

       publicMethod: function () {
           console.log('I\'m public!');
           console.log('I\'ll call a private method!');
           privateMethod();
       },

       publicProperty: 1.68,

       getPrivateProperty: function () {
           return privateProperty;
       },

       usePublicProperty: function () {
           console.log('I\'ll get a public property...' + this.publicProperty);
       }

    }
}());

module.privateMethod(); //TypeError
module.publicProperty(); //1.68
module.usePublicProperty(); //I'll get a public property...1.68
module.getPrivateProperty(); //42
module.publicMethod(); 
/*
 * I'm public!
 * I'll call a private method!
 * I'm private
 */

有一个有点奇怪的语法,即将匿名函数包装在没有父级的函数中,但暂时忘记它(它只是在初始化后执行函数)。功能可以从使用示例中看到,但好处主要与提供简单的公共接口相关,而不涉及所有实现细节。有关该模式的更详细说明,请参见我上面提供的链接。


我希望通过这个信息,帮助您了解JavaScript的一些基本主题。


4
function Foo() {
  this.bar = 0;
  this.getBar = function () { return this.bar };
}

当您使用new关键字调用上述函数时,就像这样...
var foo = new Foo();

当执行以下代码时,会发生一些事情:

1)创建一个对象;
2)使用 this 关键字引用该对象来执行函数;
3)返回该对象。

因此,foo 成为此对象:

{
    bar: 0,
    getBar: function () { return this.bar; }
};

为什么不这样做呢:

var foo = {
    bar: 0,
    getBar: function () { return this.bar; }
};

如果只是一个简单的对象,那么你就可以这样创建。但是使用构造函数来创建对象可以让我们更方便地创建多个“相同”的对象。在JavaScript中,所有函数都是由原型属性[一个对象]创建的,并且使用该函数创建的所有对象(通过使用new关键字调用它)都链接到该原型对象。这就是为什么它很酷 - 你可以将所有常见的方法(和属性,如果你想的话)存储在原型对象中,并节省大量内存。它的工作方式如下:
function Foo( bar, bob ) {
   this.bar = bar;
   this.bob = bob;
}

Foo.prototype.calculate = function () {
  // 'this' points not to the 'prototype' object 
  // as you could've expect, but to the objects
  // created by calling Foo with the new keyword.
  // This is what makes it work.
  return this.bar - this.bob;  
};

var foo1 = new Foo(9, 5);
var foo2 = new Foo(13, 3);
var result1 = foo1.calculate();
var result2 = foo2.calculate();

console.log(result1); //logs 4
console.log(result2); //logs 10

就是这样!


2
为了更接近JavaScript中的OOP,您可能想要查看模块设计模式(例如,在这里描述)。
基于闭包效应,此模式允许在对象中模拟私有属性。
使用“私有”属性,您可以直接通过其标识符引用它们(即,没有构造函数中的this关键字)。
但是,闭包和JS中的设计模式是一个高级主题。因此,请先熟悉基础知识(也在前面提到的书中解释)。

回头再仔细研究这篇文章(它的长度足以成为一本书,但似乎并没有出版)非常有帮助。感谢您让我注意到这个问题。 - Chaosed0
@Chaosed0 欢迎!虽然关于JS设计模式的书籍/文章不多值得一读,但这本书很好。 - John Doe

2
在javascript中,this总是指向函数的所有者对象。例如,如果您在页面中定义函数foo(),则所有者是javascript对象windows;或者如果您在html元素<body>上定义foo(),则所有者是html元素body;同样,如果您在元素<a>的onclick函数中定义函数,则所有者是锚点。
在您的情况下,您正在将属性bar分配给“所有者”对象,并尝试返回局部变量bar
由于您从未定义过任何局部变量bar,因此它会提示bar未定义。
理想情况下,如果您想返回值为零,则应该将变量定义为var bar;

1

this就像是对象(变量或函数)的公共访问修饰符,而var则是私有访问修饰符

示例

var x = {}; 
x.hello = function(){
    var k = 'Hello World'; 
   this.m = 'Hello JavaScript'; 
}

var t = new x.hello(); 
console.log(t.k); //undefined
console.log(t.m); //Hello JavaScript

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