JS里面方法什么时候该放构造函数内,什么时候该放在原型上呢?

举例来说我就很好奇Array构造函数里面有3个地方可以放置方法.

  1. 构造函数属性上可以放的是of from这种不可枚举的属性
  2. 构造函数内部通过this.Fn设置的方法
  3. 在prototype上设置方法,比如Array的sort方法
    我就想问在我自己设计自定义类型构造函数的时候,什么样的方法该放到内部,什么样的方法该放到原型上呢?
    除了原型上的方法是共享引用地址而构造函数内部方法是每次实例化都要执行一次内存占用之外,还有什么条件可以让我们一定要把某个方法放到构造函数内部呢?

// 方法1
function Person() {
  this.sayName = function () {
    console.log("in Fn");
  };
}

// 方法2
function Person2() {}
Person2.prototype.sayName = function () {
  console.log("out Fn");
};

const p = new Person();
const p1 = new Person2();
p.sayName();
p1.sayName();

// 什么时候用方法1, 什么时候用方法2 呢?
阅读 4.1k
5 个回答

首先,不建议采用 ES5 的构造函数定义类的方法,建议使用新的 class 语法来定义。
然后,需要区分三个 OO 的概念(先不谈语法):方法、属性和字段

一般这么理解:

  • 方法是在类中定义的对象的行为接口
  • 字段是在类中定义的对象的数据
  • 属性是在类中定义的对象的数据接口(访问接口、存取接口等都是一个意思)

在 ES5 中这三个概念区分得并不是很明显,使用 class 语法和 get/set 语法之后,区分得更清晰了。看个示例:

class Demo {
    // 字段,等字段声明语法发布后可以在这里声明字段
    // 一般字段都是受保护的,或者直接就是 private 的
    // private field 语法发布后可以声明为 #name
    // _name;

    constructor(name) {
        // 字段
        this._name = name;
    }

    // 属性,getter/setter
    // 属性可以根据需要单独提供读/写访问器,或者二者都提供
    get name() { return this._name ?? "Anonymous"; }
    set name(value) { this._name = value; }

    // 方法,一般是一个类声明的对象行为
    greet() {
        console.log(`hello ${this.name}`);
    }
}

这个示例展示的字段、属性和方法的常见用法。注意上面一直都是说的“对象数据”、“对象行为”等,是因为要实体化类,产生对象,并在对象中使用。应该容易理解的是 :类主要是声明了内/外部接口,对象才是具体的实现,每个对象都拥有自己的数据,所以数据是每个对象的,可以理解为每个对象上都有一份拷贝。讲道理,方法应该也是每个对象的,实际上每个对象的行为都完全一致,只需要能正确的引用数据就行,语句是可以共用的。所以同一个类的所有对象拥有同一个方法,也就是说,方法只有一个拷贝。

这样一来,就容易理解,数据是附着在对象上,而方法是附着在类的原型上(对象们都有同一个原型)

接下来讨论新的情况(修改了 constructorgreet()):

class Demo {
    constructor(name) {
        this._name = name;
        this.onGreeting = () => console.log(`Hello ${this.name}`);
    }

    get name() { return this._name ?? "Anonymous"; }
    set name(value) { this._name = value; }

    greet() {
        this.onGreeting();
    }
}

const a = new Demo();
const b = new Demo();
b.onGreeting = () => console.log("How are you.");

const c = new Demo();
c.onGreeting = function () { console.log(`${this.name}!!!`); };

a.greet();  // Hello Anonymous
b.greet();  // How are you.
c.greet();  // Anonymous!!!

这里 onGreeting 是一个函数,但在 OO 概念里并不把它称为“方法”,而是称为“数据”。

onGreeting 是临时想的一个例子,看命名比较倾向于“事件”。咱们忽略事件这个事情,把它当数据就好了。

这个数据保存的是一个函数,表示打招呼的方式。也就是说,这个类定义给予用户一定的灵活性来打招呼。你看,同样是函数,onGreeting 就是数据,greet 就是方法,区别就在于它是固有行为还是可制定的行为。JS 中函数本身就是对象,可以作为数据;但在 Java 或者 C# 中,作为数据的行为是通过对象来传递的(Lambda 也是对象),所以可能会更容易区分。

由于 onGreeting 是数据,所以它是附着在对象上,可以像其他数据一样变更的。它不在类的原型上。

然后更复杂的情况来了:

class Demo {
    greet() {
        console.log("greet demo");
    }
}

const a = new Demo();
a.greet = () => console.log("custom greeting");

a.greet();  // custom greeting

这是怎么回事呢?本来 a.greet() 是方法,调用它应该输出 greet demo。但是我们给对象 a 赋了一个数据,所以现在 a.greet 是一个数据,它是一个函数。这个时候的 a.greet() 是它自己的,不是 Demo 类原型上的 .greet()。调用的时候就近原则,当然是先找自己的,找不到才会去原型上找。所以调用的就是数据 greet 函数了。这个时候原型上的 greet() 当然还是在,只是被隐藏了而已。如果我们把这个数据删了,又会调用原型链上的 greet()

delete a.greet;
a.greet();  // greet demo

JS 很灵活,所以会发生这样的事情,但是一般我们在设计/使用的时候应该适当的避免过于灵活,所以通常会约定方法不允许赋值覆盖(隐藏),但是君子协定,并不是很牢靠,看各位的意识。如果是 Java 或者 C#,直接从编译器/语法层面就阻止了这种事情发生。

就是 OOP 里静态方法还是实例方法的区别。

当你不需要 new 一个实例出来才能调用时,就放到静态方法里;反之则是实例方法。

JS 现在自己也有 class 写法了,更直观一些。

class Foo {
    constructor() {
        this.message = 'hello world';
    }

    static create() {
        return new Foo();
    }

    sayHello() {
        alert(this.message);
    }
}

Foo.create();
new Foo().sayHello();

可以看看下面的demo 不知道对你有没有帮助,目前来说 可以直接用 class,想继承写起来也方便

function A() {
  this.a = 100
}

A.prototype.b = 200

const a = new A()

console.log(a)
console.log(a, a.a, a.b, {...a})

function B() {

}

// B.prototype = new A()
B.prototype = A.prototype
B.prototype.constructor = A

const b = new B()

console.log(b, b.a, b.b, { ...b})
  1. 读取对象的属性值时: 不存在时会自动到原型链中查找
  2. 设置对象的属性值时: 不会查找原型链, 如果当前对象中没有此属性, 直接添加此属性并设置其值

我的结论是:
需要根据构造函数入参动态生成所需要的函数(属性)挂载到新创建的obj上/覆盖掉原型链上的某个方法时写在构造函数中。其他情况放在两者皆可。

测试case:

function Person() {
  // 实例的方法
  this.sayName = function () {
    console.log("in Fn");
    // 有打印结果 表示并没有覆盖掉原型链上的方法
    this.__proto__.sayName();
    // 有打印结果 表示并没有静态函数和原型链上的方法并不等价
    Person.sayName();
  };
}
// 原型链上的方法
Person.prototype.sayName = function () {
  console.log("out Fn");
};
// 真正的静态方法。。
Person.sayName = function () {
  console.log("static fn");
};
const p = new Person();
p.sayName();

在我看来就没有必须采用“在构造函数里声明并添加实例方法”的场景,如果真的要声明一个可被遍历读取的实例属性,不如:

Person.prototype.sayName = function () {
  console.log("out Fn");
};

function Person(){
    this.sayName = this.sayName;
}
推荐问题