>  기사  >  웹 프론트엔드  >  JavaScript에서 new 키워드를 사용하지 않는 이유에 대한 자세한 설명

JavaScript에서 new 키워드를 사용하지 않는 이유에 대한 자세한 설명

黄舟
黄舟원래의
2017-03-15 17:35:131578검색

JavaScript의 new 키워드는 인스턴스화와 상속을 구현할 수 있지만 개인적으로는 new 키워드를 사용하는 것이 최선의 방법은 아니며, 좀 더 친숙한 구현이 있을 수 있다고 생각합니다. 이 글에서는 new 키워드 사용 시의 문제점을 소개하고, 더 빠르고 이해하기 쉬운 구현을 제공하기 위해 new와 관련된 일련의 객체 지향 작업을 캡슐화하는 방법을 소개합니다.

전통적인 인스턴스화 및 상속

두 개의 클래스, <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() {}:function Class() {} 및 SubClass:function SubClass(){}, SubClass는 Class에서 상속해야 합니다. 전통적인 메소드는 일반적으로 다음 단계에 따라 구성 및 구현됩니다. Class의

  • 상속된속성과 메소드는 Class In에 배치되어야 합니다. SubClass

    의 프로토타입 속성
  • 은 자신의 메소드와 속성 도 자신의 프로토타입 속성

  • SubClass의 프로토타입 객체의 프로토타입(proto) 속성은 반드시 클래스의 프로토타입을 가리켜야 합니다

이렇게 하면 프로토타입 체인의 특성상 SubClass의 인스턴스는 상속을 달성하기 위해 Class Method로 역추적될 수 있습니다.

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

구체적인 예를 들어 보겠습니다. 다음 코드에서는 다음 작업을 수행합니다.

  • 정의 Human

  • 이라는 상위 클래스는 Human

  • 을 상속하는 Man이라는 하위 클래스를 정의합니다. 상위 클래스의

    생성자, 이 하위 클래스 인스턴스화

  • // 构造函数/基类
    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 솔루션입니다:

    Object.create
  • Douglas Crock
  • for

    d의 객체 방식

  • 새 키워드의 단점

"Javascript: The Good Parts"에서 Douglas는 이를 피해야 한다고 믿습니다. 새 키워드 사용:

생성자 함수를 호출할 때 새 접두사를
포함

하는 것을 잊어버린 경우 이는 새 개체에 바인딩되지 않습니다. 안타깝게도 전역 개체에 바인딩됩니다. 따라서 새 개체를 확장하는 대신 전역 변수를 방해하게 됩니다. 컴파일 경고도 없고 런타임 경고도 없습니다. (49페이지)

일반적인 아이디어는 다음과 같습니다. 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의 언어 계열에서 C++에 있었던 것처럼 우리는 이를 Java의 동생으로 추진했습니다. Douglas는 이 문제를 다음과 같이 설명했습니다. 이 간접적인 방법은 고전적인 교육을 받은 프로그래머에게 언어를 더 친숙하게 보이도록 하기 위한 것이었지만 Java 프로그래머가 JavaScript의 생성자 패턴에 대해 가지고 있는 매우 낮은 의견에서 알 수 있듯이 그렇게 하지 못했습니다. 또한 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.