首页 > 新手问个JS原型问题

新手问个JS原型问题

var aa=function(){
    this.k=function(){}
}
aa.prototype={
    k:function(){}
}
aa.prototype.k=function(){}

三种方式定义方法有什么不同
为什么很多人都喜欢把属性放在构造函数里,而把方法放在原型里


为什么很多人都喜欢把属性放在构造函数里,而把方法放在原型里

因为,在最常见的情况下,每个实例的属性可能多种多样,而每个实例的方法都是一样的

所以,把属性放在构造函数里,就可以在每次构造(实例化)一个对象的时候,为其设置不同的属性;但没有必要每次实例化的时候,都在构造函数里为其设置一次方法。

三种方式定义方法有什么不同

第二种和第三种的不同:

var Human = function(){};
// 写法 a.
Human.prototype.eat = function( food ){
    console.log( '我在吃 ' + food );
};
// 写法 b.
Human.prototype = {
    sing: function( song ){
        console.log( '我在唱 ' + song );
    }
};
var james = new Human();
james.sing('小苹果'); // 我在唱 小苹果
james.eat('小苹果'); // undefined is not a function

如上,写法 b. 把类的原型指向了一个新的对象,新的对象没有 eat 方法,所以返回了 undefined is not a function 错误。所以,在你的项目里,写法 b.的代码永远不要写两次。

写法 a. 的优势是能够动态(随时随地)地给原型添加方法,但这是不推荐的。毕竟原型是大家的“共同财产”,随便乱动是不好的嘛。


好吧,我来给你讲讲。
先整理一下楼主的问题:

function A() {
  this.s = function() {};
}

function B() {
}
B.prototype = {
  s: function() {}
}

function C() {
}
C.prototype.s = function();

var a = new A(),
    b = new B(),
    c = new C();

首先说一下 A 与其他两个的区别:

如果函数 A 不作为构造函数进行调用的话,函数 A 的作用就是为当前上下文环境增加一个 s 变量。如果函数 A 作为构造函数进行调用的话,为一个新 Object 对象的上下文环境增加一个 s 变量。也就是说:

var a = new A();

与如下调用完全等价:

var a = {};
A.call(a);

当然,对所有的函数而言,如上两种调用都是等价的。

再说一下函数 B 和函数 C 的区别

函数 B 和函数 C 作为普通函数调用的话是没有什么特殊作用的。我们这里只讲一下他们作为构造函数调用的情况。
函数 B 和函数 C 的区别就是函数 B 的 prototype 对象被重写了,而函数 C 的 prototype 对象被添加了一个新的属性(方法)。这样导致的后果就是函数 B 的 prototype 对象的 constructor 属性变成了 Object。这就导致构造函数 B 产生的对象无法进行「反射」。

b.constructor === Object // true
c.constructor === C // true

所以不建议使用 B 的这种偷懒的方式来进行方法定义。

现在再来回答一下楼主的「为什么很多人都喜欢把属性放在构造函数里,而把方法放在原型里」这个问题

函数是一个「复杂类型」,它相对与其他简单的字面量类型(如数字,字符串,列表等)来说是比较占用内存的,如果把它放在构造函数里进行定义的话,每一个通过构造函数构造出来的对象都会对其进行一次完整的复制,无法对其进行共用,这是十分浪费内存的。这是原因之一。
如果把函数绑定到构造函数原型里进行共用的话,除了节省内存以外,还有一个优点就是可以达到继承的目的。还有就是可以做到「牵一发而动全身」的牛逼作用,也就是说更改一下此函数会影响到所有的此构造函数构造出来的对象。


第一种方式是把属性添加在了 new aa(); 之后的实例上;第二、第三种是添加到了构造函数 aa() 的原型上。如果对实例使用 .hasOwnProperty() 方法来检测属性的话,第一种返回 true,后两种都返回 false

访问一个对象的属性,首先查找它本身的,如果没有就查找构造函数原型上的,直到查找到 Object.prototype,这就是「原型链」。

// 定义构造函数
var aa = function() {
  // 向实例添加属性 k
  this.k = function() {};
};

// 覆盖默认的原型
aa.prototype = {
  l: function() {}
};

// 向原型添加属性
aa.prototype.m = function() {};

// 创建实例
var inst = new aa();

console.log(inst.hasOwnProperty("k")); // true
console.log(inst.hasOwnProperty("l")); // false
console.log(inst.hasOwnProperty("m")); // false

第二种是用新的对象覆盖 aa() 的默认原型对象,第三种是直接在默认的 aa() 的原型对象上添加属性。

原型默认是一个空的 Object() 实例。

// 定义构造函数
var bb = function() {};

console.log(bb.prototype); // Object {}

// 向原型添加属性
bb.prototype.cc = function() {};

console.log(bb.prototype); // Object {cc: function}

// 覆盖默认的原型
bb.prototype = Array.prototype;

console.log(bb.prototype); // []

因为每个实例的属性值很可能不同(例如经典的 Person() 实例的 name 属性),所以普通的属性(值为非 function 的属性)就在构造函数中直接赋值给 this,方法(值为 function 的属性)则在构造函数的 prototype 上定义。


前面的都讲的挺清楚的,还有一点补充的就是,如果你用第二种方式的话,要自己去维护一下原型链中的constructor

代码:

var aa=function(){
    this.k=function(){}
}
aa.prototype={
    constructor: this,
    k:function(){}
}


第一个是创建一个构造函数

第二个是在构造函数aa的原型中添加一个k方法,但是由于使用{}添加方法,会导致构造函数的constructor的指向变为Object

第三个同样是在构造函数aa的原型中添加一个k方法。但不会使构造函数的属性发生任何改变。

至于为什么很多人把属性添加在构造函数中,而把方法添加在构造函数的原型中,是因为如果大部分属性可能会被后面生成的实例所用到,那么将属性添加至构造函数中,就在在生成实例的时候同时将属性赋予实例,而不需要在实例执行的时候逐层往上查找属性。

而把方法添加至原型中是因为,如果定义的方法众多,而并不是每个方法在后面的实例中都会被用到,这样把方法定义在原型上的好处是不会每次生成实例的时候都会带有众多的方法,而是在需要的时候再逐层往上查找原型链而使用该方法,对性能有一定的提高。


这篇文章讲的挺全的:http://blog..com/muse/1190000000602050

【热门文章】
【热门文章】