龙空技术网

JavaScript (ES5) 中的闭包和原型继承

zuoandwang 97

前言:

眼前你们对“闭包原型链”都比较关切,同学们都需要知道一些“闭包原型链”的相关资讯。那么小编同时在网摘上收集了一些有关“闭包原型链””的相关资讯,希望你们能喜欢,看官们快快来了解一下吧!

JavaScript 是十分灵活的语言,这种灵活性来源于它的动态语言的本性。对于熟悉面向对象编程的开发人员来说,在使用JS来实现类和对象的编程过程中,可能会遇到一些困惑。比如闭包和原型继承就是JavaScript中很典型的两个特性,本文就这两个特性进行详细阐述。

JavaScript使用function关键字来声明函数,每个函数都是一个Function对象,创建函数主要有两个用途:

函数包含一段代码,可以直接使用functionName(args…)来调用并接收返回值函数提供一个对象的封装,可以通过 new functionName(args…) 来创建新的实例对象,在ES5中没有class的概念,我们所创建的对象都是通过function来实现的(但class在ES5是个保留的关键字)。

本文从函数的这两个主要用途开始,一步一步的介绍闭包和原型继承。

JavaScript的闭包

先来看看下面的例子:

function customSort(){    var strs = ['Fred', 'Michle', 'Tom'];    var innerSort =  function(){        var sorted = strs.sort();        //maybe more logics here...        return sorted;    };    return innerSort;}var f = customSort (); // innerSortf(); // 调用函数f时才进行排序// output: ["Tom", "Fred", "Michle"]

在这个例子中,函数customSort中又定义了函数innerSort,并且内部函数innerSort可以引用外部函数customSort的参数和局部变量,当customSort返回函数innerSort时,相关参数和变量都保存在返回的函数中,这种程序结构被称为“闭包(Closure)”

闭包不只是返回一个函数这么简单,试想在高级开发语言C#/Java中有private关键字可以实现私有变量,再看看上面的例子中变量strs的作用域, 相当于”私有化”了strs变量! 这样我们可以在实际的开发过程中使用闭包来私有化一些不想被外界使用的对象,换句话说:闭包就是携带状态的函数,并且它的状态可以对外隐藏起来。

静态变量与实例变量

闭包的概念在JS中是很重要的,通过闭包的特性来实现私有变量的封装,巧妙的实现了JS中对面向对象的封装性的实现,在这里我们要介绍的是关于静态变量(类变量)和实例变量(对象变量)的实现:

在上面代码中,我们定义了类User,其构造函数是内部定义的UserClass函数。在这里我们注意到,变量_counter定义在UserClass之外,而变量_firstName,_lastName是定义在UserClass之内的,由于闭包的特性,这些变量都被封装在User之内,因此可以说他们都是私有变量,然而不同的是,_counter相对于User类来说,是静态变量,而_firstName和_lastName是实例变量,这是因为当我们实例化User的时候,每次调用构造函数UserClass,变量_firstName和_lastName都会在内存中分配新的变量,而_counter定义在UserClass之外,它在内存中只保存一个拷贝,因此相当于UserClass来说,它是”静态”的。

下面我们来测试一下,如下面代码:

这段代码的返回结果是这样的:

可以看到,我们在调用静态方法User.getUserCount()的时候是在类User上调用的,该方法返回静态变量_counter的值,该变量累计构造User对象的个数。而实例变量_firstName, _lastName,保存的是每个实例对象中的值,在这里我们通过对象的getFullName()返回。

通过闭包,我们在JS中实现或者是模拟出面向对象语言中的私有变量,静态变量,实例变量等特性,静态变量与实例变量的恰当使用,可以使我们更优化的利用内存,以及实现完成某些逻辑需求。

JavaScript的原型继承

我们从封装一个User对象开始:

function User(firstname, lastname){	this.firstname = firstname;  	this.lastname = lastname;}User.prototype.getFullName = function(){	return this.firstname + ' ' + this.lastname;};var user = new User('Fred', 'Lee');console.log(user.firstname); // Fredconsole.log(user.lastname);  // Leeconsole.log(user.getFullName()); // Fred Lee

这里我们通过new关键字来创建User的一个实例:var user = new User(‘Fred’, ‘Lee’); 在这个过程中,User对象的构造函数(也就是User函数本身,可以通过User.prototype.constructor确认)被调用。

JavaScript中this 关键字代表当前执行的上下文环境;

默认情况下,例如在customSort这个例子中,this就被设置成了当前运行环境的全局变量(在浏览器中是window, 在nodejs环境中是global)。在通常情况下,我们通过一个表达式 user.getFullName() 来调用函数:即从一个对象的属性中得到所调用的函数。此时this被设置为我们取得函数的对象(即user)。在这个例子中,this指的就是user这个实例。我们也可以通过使用functionName.call(context, arg1, arg2…) 或者functionName.apply(context, [arg1, arg2])来改变函数的执行上下文(也就是this)。

接下来我们想创建一个Manager对象,需要说明的是: Manager也是一个User,跟User不同的是Manager可能有一些特殊的工作要做,毫无疑问,我们的第一反应是继承,那在JavaScript里面如何实现类似于C#/Java的继承呢?

我们一步一步来看:

把firstname, lastname等User的属性继承下来,要达到这个目的,我们可以有以下两种做法:

第一种:在Manager里面把firstname, lastname重新定义一遍 (NO!,麻烦、重复代码多,而且这也算不上是“继承”)联系到关于this的特性,我们可以在Manager的“构造函数”里面直接用User.call(this, firstname, last)的方式把User对象中的firstname和lastname属性“复制”到Manager对象中,实现方式如下:

function Manager(firstname, lastname){	User.call(this, firstname, lastname);}var manager = new Manager('Man', 'ger');console.log(manager.firstname); // Manconsole.log(manager.lastname);  // gerconsole.log(manager.getFullName()); // error: manager.getFullName is not a function

注意最后一句manager.getFullName()抛出了TypeError: manager.getFullName is not a function, 也就是说定义在User.prototype中的方法是不能通过User.call(this, …)继承的;别急,看下一步。

第二种:把User.prototype中的方法“继承”到Manager对象。这一步比较复杂难以理解,我们先来讲一下JavaScript中的prototype(原型):

每一个JavaScript对象都有一个叫做prototype的属性,比如User.prototype 表示是对象User的原型对象;JavaScript在调用一个对象实例的属性或者方法的时候按照下面的顺序查找,直到对象的原型链上最后一个原型指向null为止。

具体到User对象的实例user,当我们使用user.firstname的时候,由于User对象本身就有一个叫做firstname的属性,所以直接返回。当调用user.getFullName()的时候,首先查找User对象本身,没有找到方法getFullName(), 所以继续在user._proto_上找,由于user._proto_有一个名字叫做getFullName()的方法,所以调用位于User原型上的user._proto_.getFullName()方法。

如果没有显式指定,JavaScript对象的原型是一个Object对象的实例,例如:User.prototype === user._proto_ 都指向一个Object实例。如果显式将JavaScript对象Manager的prototype指向User的原型prototype,即: Manager.prototype = User.prototype, 那么Manager对象的实例manager._proto_ === Manager.prototype === User.prototype, 这样实例manager将直接获得对象User原型上的方法.

function Manager(firstname, lastname){	User.call(this, firstname, lastname);}Manager.prototype = User.prototype;var manager = new Manager('Man', 'ger');console.log(manager.firstname); // Manconsole.log(manager.lastname);  // gerconsole.log(manager.getFullName()); // Man ger

注意这里实例manager已经可以直接调用getFullName()方法并正确返回了。但是这样做是不合理的!例如我们想给Manager的原型上添加一个新的方法:

function Manager(firstname, lastname){	User.call(this, firstname, lastname);}Manager.prototype = User.prototype;Manager.prototype.makePhoneCall = function(){	console.log('Manager: makePhoneCall');};

由于Manager和User共享同一个prototype,所以User.prototype上也有了一个makePhoneCall方法,这不是合理的继承!

再来看这种修改原型链的方式:

function Manager(firstname, lastname){	User.call(this, firstname, lastname);}Manager.prototype = new User('fakeFirstname', 'fakeLastname');var manager = new Manager('Man', 'ger');console.log(manager.firstname); // Manconsole.log(manager.lastname);  // gerconsole.log(manager.getFullName()); // Man ger

这个做法可以达到通过原型链继承User的目的,但是有两个问题:首先,需要多实例化一个User对象;其次,如果User的构造器带有参数,无法确定这个应该给的参数是什么。

上面已经逐步地介绍了实现原型继承的基本原理,即通过某种手段使“子类实例”的原型对象能够指向其“父类”的原型。

在JavaScript中实现原型继承的方法有很多,这里介绍一种比较常用的做法,前面已经说了通过共享原型来实现继承是不合理的,那么我们可以通过创建一个中间对象来实现正确的原型链(TypeScript在把继承编译成JS代码的时候也是这么做的):

function Manager(firstname, lastname){	User.call(this, firstname, lastname);}function __() { 	this.constructor = Manager;  //修正在改变原型的过程中导致的constructor改变}__.prototype = User.prototype;Manager.prototype = new __();Manager.prototype.makePhoneCall = function(){	console.log('Manager: makePhoneCall');};

需要注意的是在function __中的this.constructor = Manager; 这句代码将Manager.prototype.constructor重新置为Manager;这里中间函数__()的实例仅用来桥接,我们并没有改变User.prototype; 下面验证继承的实现:

console.log(Manager.prototype.constructor); // function Manager()...console.log(user instanceof User);  // trueconsole.log(manager instanceof Manager);  // trueconsole.log(manager instanceof User);  // true

到目前为止,实例manager的原型链是这样的:

但是这样的继承就是完美的吗?假设User对象有一个非实例属性或者方法,看下面的例子:

function User(firstname, lastname){	this.firstname = firstname;  	this.lastname = lastname;}User.staticProperty = 'static';User.staticMethod = function(){	console.log('User.staticMethod');};User.prototype.getFullName = function(){	return this.firstname + ' ' + this.lastname;};function Manager(firstname, lastname){	User.call(this, firstname, lastname);  this.age = 20;}function __() { 	this.constructor = Manager; }__.prototype = User.prototype;Manager.prototype = new __();Manager.prototype.makePhoneCall = function(){	console.log('Manager: makePhoneCall');};console.log(User.staticProperty);  // staticUser.staticMethod();  // User.staticMethodconsole.log(Manager.staticProperty);  // undefinedManager.staticMethod();  // Error: Manager.staticMethod is not a function

在“继承”之后,Manager.staticProperty和Manager.staticMethod() 都是不存在的!这也是不合理的!所以需要对我们实现的这个“继承”进一步的完善;由于User.staticProperty和User.staticMethod()是属于所有User实例的对象级属性/方法,我们需要对这些属性/方法做特殊处理:在继承之后将这些属性/方法复制到子类中,完善后的完整代码:

function User(firstname, lastname){	this.firstname = firstname;  	this.lastname = lastname;}User.staticProperty = 'static';User.staticMethod = function(){	console.log('User.staticMethod');};User.prototype.getFullName = function(){	return this.firstname + ' ' + this.lastname;};function Manager(firstname, lastname){	User.call(this, firstname, lastname);}for (var p in User) {	if (User.hasOwnProperty(p)) {  	Manager[p] = User[p];  }}function __() { 	this.constructor = Manager; }__.prototype = User.prototype;Manager.prototype = new __();Manager.prototype.makePhoneCall = function(){	console.log('Manager: makePhoneCall');};console.log(User.staticProperty);  // staticUser.staticMethod();  // User.staticMethodconsole.log(Manager.staticProperty);  // staticManager.staticMethod();  // User.staticMethod

这样就实现了对这些“静态”属性/方法的“继承”(确切来说,这不叫继承,这其实就是对象属性的复制)。

每次需要实现继承的时候都要写这么多的代码太麻烦了,我们可以把整个“继承”的过程封装一下:

window.__extends = window.__extends || function (d, b) {        for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];        function __() { this.constructor = d; }        d.prototype = (b === null ? Object.create(b) : (__.prototype = b.prototype, new __()));};// 可以将上面User和Manager之间的继承简化为:function Manager(firstname, lastname) {	User.call(this, firstname, lastname);}__extends(Manager, User);Manager.prototype.makePhoneCall = function() {	console.log('Manager: makePhoneCall');};

结合闭包和原型继承,我们可以进一步优化上面的代码,最后User和Manager之间的继承可以写成:

window.__extends = window.__extends || function (d, b) {    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];    function __() { this.constructor = d; }    d.prototype = (b === null ? Object.create(b) : (__.prototype = b.prototype, new __()));};var User = (function () {    function User(firstname, lastname) {        this.firstname = firstname;        this.lastname = lastname;    }    User.prototype.getFullName = function () {        return this.firstname + ' ' + this.lastname;    };    return User;}());var Manager = (function (_super) {    __extends(Manager, _super);    function Manager(firstname, lastname) {        _super.call(this, firstname, lastname);    }    Manager.prototype.makePhoneCall = function () {        console.log('Manager: makePhoneCall');    };}(User));

在上面的代码中,Function类型的变量User和Manager各自被自执行的函数封装,在Manager内部没有任何关于其“父类”User的信息,只有一个_super,我们可以通过_super.call(this, firstname, lastname)来使用其“父类”User的构造函数初始化,也可以在实例函数中使用_super.prototype.getFullName.call(this)来调用“父类”User的getFullName方法。看起来是不是很接近C#/Java等面向对象语言的继承了,有木有?

我们尝试去实现面向对象编程中的继承特性,是为了给我们的编程带来强大的功能,能更好的封装和解耦代码之间的关系。JS是十分灵活的语言,我们可以通过多种方式去实现或者模拟面向对象的特性,在上面的实践中,我们重点讲了如何通过prototype去实现继承,正是由于JS本身的灵活性,在实现的过程中,也许会出现各种问题,希望读者能仔细研究前面介绍的方法,多加实践,这样才能更加灵活的掌握,在实际工作中更加自如的运用。

标签: #闭包原型链