js:在原型函数中访问构造器中的var变量,该变量在所有实例中被共享

1.问题描述
看代码以及执行结果能够更好滴说明问题

  1. 代码及执行结果
<script>

    function A(par)
    {
        var i = par ;
        A.prototype.getI = function ()
        {
            return i ;
        }

    }

    function B()
    {
        var a1 = new A(1);
        var a2 = new A(2);

        console.log(a1.getI());
        console.log(a2.getI());
    }

    B() ;

</script>

期待结果: 输出 1,2
实际结果: 输出 2,2

  1. 疑问
    i存在于每个实例中,保存了不同的值。

但是通过原型函数返回的是最后一个实例的i值。
这是为什么?

  1. 猜测
    有朋友说原型函数中i实际是引用,因为原型函数被定义在了构造器中,因此每次实例化,i的引用都会被更行,因此它指向了最后一个实例中的i的值。我没有在网络上查到确切的说明。但是该解释可以被理解

参考URL: http://ask.csdn.net/questions...

阅读 4.9k
8 个回答

这个问题极好!你需要理解如下知识

  1. 闭包
  2. new 到底做了什么,参看 ecma262 new operator
  3. 作用域链
  4. 原型链
  5. this
  6. 执行环境 context

如果没时间请阅读这篇文章 closure for fun
如果有时间请阅读 Dmitry Soshnikov 理解 js 系列

导致这个问题的原因是这个现象 闭包作用域的共享

那么解决手段有两种

  1. 常规方法,使用 this.i 定义变量,并利用 this.i 返回
  2. 采用 this.getI 定义两个闭包结构 (不推荐此方法,只是为了说明原理)

此外对于引用类型理解,参看 Nicholas.C.Zakas 大作
The Principles of Object-Oriented JavaScript 第一章

图片描述

一定要认识到 js 里面,除了基本类型一切皆对象,特别是函数.

你如果想实现类似 c++ 的权限控制,必须理解闭包.或者采用 _xx 这种约定方式编写.

给一个实现私有化的范例函数.

 var A =    (function() {
     var privateStor = {};
     var uid = 0;


    function A(par)
    { 
        privateStor[this.id = uid++] = {};
        privateStor[this.id].i = par;
      
        A.prototype.getI = function ()
        {
            return privateStor[this.id].i;
        }

    }
    return A

 })();
   function B()
    {
        var a1 = new A(1);
        var a2 = new A(2);

        console.log(a1.getI());
        console.log(a2.getI());
    }
    B() ;

因为你的这段代码,是在A的原型上定义了一个方法,但是其中的i,其实是来源于定义时的父级作用域,并且实例化时候会被重写。
那么这里其实是输出的最后一次实例化时候的i,也就是2。

A.prototype.getI = function () {
    return i ;
}

以上解释和你的猜测基本一致。
一般的定义原型方法其实是在构造函数外进行的,并且是通过this.i这样传递值。

return this.i

主要原因在于,你在构造函数里面去添加原型方法。
每次new操作,相当于这个原型方法都需要被重新赋值。

 function A(par)
    {
        var i = par ;
        A.prototype.getI = function ()
        {
            return i ;
        }

    }

每次new A操作,getI都会被重新赋值,里面的i所取的值必然跟着变了。(getI是挂在A的prototype属性上,所有实例共享该方法)

update

其实如果要达到你的目的,只要不把get函数挂到原型上面去就OK了。(getI现在是挂在每个new出来的实例上)

 function A(par)
{
    var i = par ;
    this.getI = function ()
    {
        return i ;
    }

}
function A(par)
{
    var i = par ;
    A.prototype.getI = function ()
    {
        return this.i ;//this.i 表示当前构造函数的值i
    }

}

function B()
{
    var a1 = new A(1);
    var a2 = new A(2);

    console.log(a1.getI());
    console.log(a2.getI());
}

B() ;//undefined  undefined 

///

function A(par)
{
    var i = par ;

}
 A.prototype.getI = function (i)//把A的原型方法写在外面的话,执行效果跟上面一样
    {
        return i ;
    }
function B()
{
    var a1 = new A(1);
    var a2 = new A(2);

    console.log(a1.getI());
    console.log(a2.getI());
}

B() ;//undefined  undefined 

//

function A(par)
{
    this.i = par ;
    this.getI=function(){//构造函数的方法
        return this.i;
    }
}

function B()
{
    var a1 = new A(1);
    var a2 = new A(2);

    console.log(a1.getI());
    console.log(a2.getI());
}

B() ;// 1  2

//

   function A(par)
    {
        var i = par ;
        A.prototype.getI = function ()
        {
            return i ;
        }

    }

    function B()
    {
        var a1 = new A(1);
        var a2 = new A(2);
        var a1 = new A(1);//在题主的基础上增加这个
        console.log(a1.getI());
  
    }

    B() ;//最终输出 1
    总结:没总结。

在构造器中定义var局部变量,并将原型函数也定义在构造器中,其目的确实是想让局部变量能够被全部原型函数所共享,从而var局部变量完成了“私有变量”的功能。

但是定义在构造器中的原型函数在每次实例化的时候,都被重新定义,显然这不是期待的结果。

如果将原型函数移出构造器,那么,要想在所有原型函数中共享变量,就只能定义公有属性(this.xxx)。

但是,有些变量只希望内部共享,而不向外部公开。比如:某原型函数是个递归函数,为了避免每次递归都生成临时局部变量(递归次数多的时候这些变量将带来内存占用),因此需要在外部提供一个可用的变量,例如定义 tempI,tempJ,tempLength等等。如果将这些变量定义为公有属性:this.tempI 之类的话,从代码封装角度来看,无疑是不好的。

ES6中提供了class语义,但不提供private语义,我猜想是受到js本身设计的局限,根本上无法实现private功能。

==================================================================
根据朋友们上面的回答,我理解如下:

就示例代码而言,原型函数getI被定义在了构造器中,因此能够访问到var变量i。
当代码执行到a1.getI()的时候,其内部代码 【return i】 的i变量就是实例a1中存储的值:1。
当代码执行到a2.getI()的时候,其内部代码 【return i】 的i变量就是实例a2中存储的值:2。
上面的预期结果是最容易想到,虽然这是错误的预期。

既然预期不正确,那么错在哪里呢?
原型函数被所有实例共享,从示例代码的执行结果来看,getI()的内部代码 【return i】 中的i变量的值也被全部实例“共享”了。
怎么被共享的呢?

【推测一】
就是【return i】 中的i变量实际是一个引用变量,因此无论哪个实例使用的都是同一个引用地址,也就是最后被实例化的a2中的变量i的地址。
这个猜测比较容易从道理上被理解的。
但是,朋友【zenHeart】也提出了,示例代码 【return i】 中的i变量并不符合js对‘引用变量’的定义,对此我也是这么认为。

如果推测一是不正确的,那么又是哪里错了呢?
【推测二】
js中,将变量与js运行时的实例上下文关联起来的只有【this】,除此之外,变量都和上下文无关联。因此原型函数代码 【return i】中的i不能和运行时实例上下文产生关联,而是个“特定的值”。因为示例代码的原型函数被定义在构造器中,因此每次实例化,原型函数都被重新定义,其内部的变量就是最后一次实例化后实例体中的变量i的值。
但是,js是在运行时(而非定义时)动态寻找变量的,因此,理论上,实例化a1,a2的过程并没有对 【return i】 中的i产生影响。也即是说只有当执行a1.getI()、a1.getI()的时候,js才动态地去确定【return i】 中的i到底在哪里,值是什么。在原型链中寻找变量也是这个道理 --- 这是我个人理解,也许不正确,请指正。

如果我的理解是正确的,那么js解释器在动态寻找i的时候,为什么找到了a2中的i,而不是a1中的i?

以下2图是我认为的【推测二】的一些依据
图片描述
图片描述

简单理解一下即可。

改成下面的顺序,会输出期望的结果,因为如果不这样子写,getI()方法是定义在原型中,定义在原型中的方法,是所有的实例共用的,一旦这个方法被其他实例改动,则这个方法就会变动。

因此,当你new(A(2))的时候,这个方法已经发生了变化,返回的值,也是重新覆盖的par。

    function B() {
        var a1 = new A(1);
        console.log(a1.getI());
        var a2 = new A(2);
        console.log(a2.getI());
    }

同样的,方法(function)是对象、数组也是对象,这些定义在原型中,都属于引用。

    function B() {
        var a1 = new A(1);
        console.log(a1.arr);
        var a2 = new A(2); // [1, 2, 3, 4]
        a2.arr.push(5);
        console.log(a1.arr);// [1, 2, 3, 4, 5]
        console.log(a2.arr); // [1, 2, 3, 4, 5]
    }

    B();

上面定义arr如果改一改:不定义在原型上

function A(par) {
        var i = par;
        A.prototype.getI = function () {
            return i;
        }
        this.arr = [1,2,3,4];
    }

    function B() {
        var a1 = new A(1);
        console.log(a1.arr);
        var a2 = new A(2); // [1, 2, 3, 4]
        a2.arr.push(5);
        console.log(a1.arr);// [1, 2, 3, 4]
        console.log(a2.arr); // [1, 2, 3, 4, 5]
    }

    B();

在A方法中 你直接修改了原型上的方法 所以后一个覆盖了前一个

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题