ホームページ  >  記事  >  ウェブフロントエンド  >  JavaScript で new キーワードを使用しない理由の詳細な説明

JavaScript で new キーワードを使用しない理由の詳細な説明

黄舟
黄舟オリジナル
2017-03-15 17:35:131639ブラウズ

JavaScript の new キーワードはインスタンス化と継承を実装できますが、個人的には new キーワードの使用はベスト プラクティスではなく、より使いやすい実装があると思います。この記事では、new キーワードの使用に関する問題を紹介し、その後、new に関連付けられた一連の オブジェクト指向 操作をカプセル化し、より高速で理解しやすい実装を提供する方法を紹介します。

従来のインスタンス化と継承

2 つのクラス、<a href="http://www.php.cn/wiki/164.html" target="_blank">Class<code><a href="http://www.php.cn/wiki/164.html" target="_blank">Class</a>:function Class() {}SubClass:function SubClass(){}: function Class( ) {} と SubClass:function SubClass(){} の場合、サブクラスはクラスから継承する必要があります。従来のメソッドは一般に次のように構成され、実装されます:

  • 継承されたプロパティとメソッドは、Class

  • SubClass

    独自のメソッドのプロトタイプ属性に配置する必要があり、属性も配置する必要があります自身のプロトタイプ属性

  • SubClassのプロトタイプオブジェクトのプロトタイプ(proto)属性は、クラスのプロトタイプを指している必要があります

このように、プロトタイプチェーンの特性により、サブクラスのインスタンスを追跡することができます継承を実現するために Class メソッドに追加します:

new SubClass()      Object.create(Class.prototype)
    |                    |
    V                    V
SubClass.prototype ---> { }
                        { }.proto ---> Class.prototype

具体的な例を示します: 次のコードでは、次のことを行います:

  • Human

  • という名前の親クラスを定義します

  • Man という名前のクラスを定義します サブクラスは以下を継承しますHuman

    サブクラスは親クラスのすべての属性を継承し、親クラスの
  • コンストラクター
を呼び出してこのサブクラスをインスタンス化します

// 构造函数/基类
function Human(name) {
    this.name = name;
}

/* 
    基类的方法保存在构造函数的prototype属性中
    便于子类的继承
*/
Human.prototype.say = function () {
    console.log("say");
}

/*
    道格拉斯的object方法(等同于object.create方法)
*/
function object(o) {
    var F = function () {};
    F.prototype = o;
    return new F();
}

// 子类构造函数
function Man(name, age) {
    // 调用父类的构造函数
    Human.call(this, name);
    // 自己的属性age
    this.age = age;
}

// 继承父类的方法
Man.prototype = object(Human.prototype);
Man.prototype.constructor = Man;

// 实例化子类
var man = new Man("Lee", 22);
console.log(man);
// 调用父类的say方法:
man.say();

DEMO
  • 上記のコードを通じて、インスタンス化の伝統的ないくつかの特徴を要約することができますおよび継承:

  • 従来のメソッドの「クラス」はコンストラクターである必要があります。

  • 属性とメソッドはプロトタイプ属性にバインドされ、継承はプロトタイプの特性を利用して実装されます。

new キーワードを使用してオブジェクトをインスタンス化します。

  • なぜ Object.create メソッドが Douglas のオブジェクト メソッドと一致していると確信できるのでしょうか? MDN では、オブジェクト メソッドは Object.create の Polyfill ソリューションであるためです。 : The Good Parts" では、Douglas は、新しいキーワードの使用は避けるべきであると考えています:

  • コンストラクター関数を呼び出すときに新しい接頭辞を
  • インクルード

    するのを忘れると、これは新しいオブジェクトにバインドされません。悲しいことにこれはグローバル オブジェクトにバインドされるため、新しいオブジェクトを拡張する代わりに、グローバル変数を上書きすることになります。これは、コンパイル時の警告も、実行時の警告もありません

    。一般的な考え方は、new を使用する必要があるときに new キーワードを忘れると、何らかの問題が発生するということです。
もちろん、キーワードを使用し忘れると、一連の問題が発生します。一歩下がって考えると、この問題は完全に回避可能です:

function foo()
{   
   // 如果忘了使用关键字,这一步骤会悄悄帮你修复这个问题
   if ( !(this instanceof foo) )
      return new foo();

   // 构造函数的逻辑继续……
}

またはより一般的な

例外をスロー

する
function foo()
{
    if ( !(this instanceof arguments.callee) ) 
       throw new Error("Constructor called as a function");
}

か、John Resig の計画に従って、ほとんどの初期化関数を実行する makeClass ファクトリ関数を準備します それを init に置きますコンストラクター自体ではなく、メソッドです:

// makeClass - By John Resig (MIT Licensed)
function makeClass(){
  return function(args){
    if ( this instanceof arguments.callee ) {
      if ( typeof this.init == "function" )
        this.init.apply( this, args.callee ? args : arguments );
    } else
      return new arguments.callee( arguments );
  };
}
私の意見では、new キーワードが良い習慣ではない主な理由は次のとおりです:

…new は、JavaScript が「人気」を得るために Java のような構文を受け入れた時代の名残です。そして、Visual Basic のような補完的な言語が当時の Microsoft の言語ファミリーであったように、私たちはそれを Java の弟として推し進めていました

Douglas は、この問題を次のように説明しました。この言語は、古典的な訓練を受けたプログラマにとってはより馴染みのあるものに見えますが、JavaScript に対する Java プログラマの評価が非常に低いことからわかるように、JavaScript のコンストラクタ パターンは古典的な人々の興味を引くものではありませんでした。その結果、この言語を効果的に使用する方法を知っているプログラマーはほとんどいません

簡単に言うと、JavaScript は市場に応えるために作成された典型的な言語です 新しいキーワードは、人々に考えさせる必要があるために導入されました。それはJavaに似ているということです。 JavaScript は、そのプロトタイプ機能を通じてインスタンス化と継承を実装することになっていますが、新しいキーワードにより、それが目立たなくなります。

把传统方法加以改造

既然new关键字不够友好,那么我们有两个办法可以解决这个问题:一是完全抛弃new关键字,二是把含有new关键字的操作封装起来,只向外提供友好的接口。下面将介绍第二种方法的实现思路,把传统方法加以改造。

我们开始构造一个最原始的基类Class(类似于JavaScript中的Object类),并且只向外提供两个接口:

  • Class.extend 用于拓展子类

  • Class.create 用于创建实例

// 基类
function Class() {}

// 将extend和create置于prototype对象中,以便子类继承
Class.prototype.extend = function () {};
Class.prototype.create = function () {};

// 为了能在基类上直接以.extend的方式进行调用
Class.extend = function (props) {
    return this.prototype.extend.call(this, props);
}

extend和create的具体实现:

Class.prototype.create = function (props) {
    /*
        create实际上是对new的封装;
        create返回的实例实际上就是new构造出的实例;
        this即指向调用当前create的构造函数;
    */
    var instance = new this();
    /*
        绑定该实例的属性
    */
    for (var name in props) {
        instance[name] = props[name];
    }
    return instance;
}

Class.prototype.extend = function (props) {
    /*
        派生出来的新的子类
    */
    var SubClass = function () {};
    /*
        继承父类的属性和方法,
        当然前提是父类的属性都放在prototype中
        而非上面create方法的“实例属性”中
    */
    SubClass.prototype = Object.create(this.prototype);
    // 并且添加自己的方法和属性
    for (var name in props) {
        SubClass.prototype[name] = props[name];
    }
    SubClass.prototype.constructor = SubClass;

    /*
        介于需要以.extend的方式和.create的方式调用:
    */
    SubClass.extend = SubClass.prototype.extend;
    SubClass.create = SubClass.prototype.create;

    return SubClass;
}

仍然以Human和Man类举例使用说明:

var Human = Class.extend({
    say: function () {
        console.log("Hello");
    }
});

var human = Human.create();
console.log(human)
human.say();

var Man = Human.extend({
    walk: function () {
        console.log("walk");
    }
});

var man = Man.create({
    name: "Lee",
    age: 22
});

console.log(man);
// 调用父类方法
man.say();

man.walk();

DEMO

至此,基本框架已经搭建起来,接下来继续补充功能。

  1. 我们希望把构造函数独立出来,并且统一命名为init。就好像Backbone.js中每一个view都有一个initialize方法一样。这样能让初始化更灵活和标准化,甚至可以把init构造函数借出去

  2. 我还想新增一个子类方法调用父类同名方法的机制,比如说在父类和子类的中都定义了一个say方法,那么只要在子类的say中调用this.callSuper()就能调用父类的say方法了。例如:

// 基类
var Human = Class.extend({
    /*
        你需要在定义类时定义构造方法init
    */
    init: function () {
        this.nature = "Human";
    },
    say: function () {
        console.log("I am a human");
    }
})

var Man = Human.extend({
    init: function () {
        this.sex = "man";
    },
    say: function () {
        // 调用同名的父类方法
        this.callSuper();
        console.log("I am a man");
    }
});

那么Class.create就不仅仅是new一个构造函数了:

Class.create = Class.prototype.create = function () {
    /*
        注意在这里我们只是实例化一个构造函数
        而非最后返回的“实例”,
        可以理解这个实例目前只是一个“壳”
        需要init函数对这个“壳”填充属性和方法
    */
    var instance = new this();

    /*
        如果对init有定义的话
    */
    if (instance.init) {
        instance.init.apply(instance, arguments);
    }
    return instance;
}

实现在子类方法调用父类同名方法的机制,我们可以借用John Resig的方案:

Class.extend = Class.prototype.extend = function (props) {
    var SubClass = function () {};
    var _super = this.prototype;
     SubClass.prototype = Object.create(this.prototype);
     for (var name in props) {
        // 如果父类同名属性也是一个函数
        if (typeof props[name] == "function" 
            && typeof _super[name] == "function") {
            // 重新定义用户的同名函数,把用户的函数包装起来
            SubClass.prototype[name] 
                = (function (super_fn, fn) {
                return function () {

                    // 如果用户有自定义callSuper的话,暂存起来
                    var tmp = this.callSuper;
                    // callSuper即指向同名父类函数
                    this.callSuper = super_fn;
                    /*
                        callSuper即存在子类同名函数的上下文中
                        以this.callSuper()形式调用
                    */
                    var ret = fn.apply(this, arguments);
                    this.callSuper = tmp;

                    /*
                        如果用户没有自定义的callsuper方法,则delete
                    */
                    if (!this.callSuper) {
                        delete this.callSuper;
                    }

                    return ret;
                }
            })(_super[name], props[name])  
        } else {
            // 如果是非同名属性或者方法
            SubClass.prototype[name] = props[name];    
        }

        ..
    }

    SubClass.prototype.constructor = SubClass; 
}

最后给出一个完整版,并且做了一些优化:

function Class() {}

Class.extend = function extend(props) {

    var prototype = new this();
    var _super = this.prototype;

    for (var name in props) {

        if (typeof props[name] == "function" 
            && typeof _super[name] == "function") {

            prototype[name] = (function (super_fn, fn) {
                return function () {
                    var tmp = this.callSuper;

                    this.callSuper = super_fn;

                    var ret = fn.apply(this, arguments);

                    this.callSuper = tmp;

                    if (!this.callSuper) {
                        delete this.callSuper;
                    }
                    return ret;
                }
            })(_super[name], props[name])
        } else {
            prototype[name] = props[name];    
        }
    }

    function Class() {}

    Class.prototype = prototype;
    Class.prototype.constructor = Class;

    Class.extend =  extend;
    Class.create = Class.prototype.create = function () {

        var instance = new this();

        if (instance.init) {
            instance.init.apply(instance, arguments);
        }

        return instance;
    }

    return Class;
}

下面是测试的代码。为了验证上面代码的健壮性,故意实现了三层继承:

var Human = Class.extend({
    init: function () {
        this.nature = "Human";
    },
    say: function () {
        console.log("I am a human");
    }
})

var human = Human.create();
console.log(human);
human.say();

var Man = Human.extend({
    init: function () {
        this.callSuper();
        this.sex = "man";
    },
    say: function () {
        this.callSuper();
        console.log("I am a man");
    }
});

var man = Man.create();
console.log(man);
man.say();

var Person = Man.extend({
    init: function () {
        this.callSuper();
        this.name = "lee";
    },
    say: function () {
        this.callSuper();
        console.log("I am Lee");
    }
})

var person = Person.create();
console.log(person);
person.say();

DEMO

是时候彻底抛弃new关键字了

如果不使用new关键字,那么我们需要转投上两节中反复使用的Object.create来生产新的对象

假设我们有一个矩形对象:

var Rectangle = {
    area: function () {
        console.log(this.width * this.height);
    }
};

借助Object.create,我们可以生成一个拥有它所有方法的对象:

var rectangle = Object.create(Rectangle);

生成之后,我们还可以给这个实例赋值长宽,并且取得面积值

var rect = Object.create(Rectangle);
rect.width = 5;
rect.height = 9;
rect.area();

注意这个过程我们没有使用new关键字,但是我们相当于实例化了一个对象(rectangle),给这个对象加上了自己的属性,并且成功调用了类(Rectangle)的方法。

但是我们希望能自动化赋值长宽,没问题,那就定义一个create方法:

var Rectangle = {
    create: function (width, height) {
      var self = Object.create(this);
      self.width = width;
      self.height = height;
      return self;
    },
    area: function () {
        console.log(this.width * this.height);
    }
};

使用方式如下:

var rect = Rectangle.create(5, 9);
rect.area();

在纯粹使用Object.create的机制下,我们已经完全抛弃了构造函数这个概念。一切都是对象,一个类也可以是对象,这个类的实例不过是一个它自己的复制品。

下面看看如何实现继承。我们现在需要一个正方形,继承自这个长方形

var Square = Object.create(Rectangle);

Square.create = function (side) {
  return Rectangle.create.call(this, side, side);
}

实例化它:

var sq = Square.create(5);
sq.area();

这种做法其实和我们第一种最基本的类似

function Man(name, age) {
    Human.call(this, name);
    this.age = age;
}

上面的方法还是太复杂了,我们希望进一步自动化,于是我们可以写这么一个extend函数

function extend(extension) {
    var hasOwnProperty = Object.hasOwnProperty;
    var object = Object.create(this);

    for (var property in extension) {
      if (hasOwnProperty.call(extension, property) || typeof object[property] === "undefined") {
        object[property] = extension[property];
      }
    }

    return object;
}

/*
    其实上面这个方法可以直接绑定在原生的Object对象上:Object.prototype.extend
    但个人不推荐这种做法
*/

var Rectangle = {
    extend: extend,
    create: function (width, height) {
      var self = Object.create(this);
      self.width = width;
      self.height = height;
      return self;
    },
    area: function () {
        console.log(this.width * this.height);
    }
};

这样当我们需要继承时,就可以像前几个方法一样用了

var Square = Rectangle.extend({
    // 重写实例化方法
    create: function (side) {
         return Rectangle.create.call(this, side, side);
    }
})

var s = Square.create(5);
s.area();

结束语

本文对去new关键字的方法做了一些罗列,但工作还远远没有结束,有非常多的地方值得拓展,比如:如何重新定义instance of方法,用于判断一个对象是否是一个类的实例?如何在去new关键字的基础上继续实现多继承?希望本文的内容在这里只是抛砖引玉,能够开拓大家的思路。


以上がJavaScript で new キーワードを使用しない理由の詳細な説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。