深入浅出JS的封装与继承

JS虽然是一个面向对象的语言,但是不是典型的面向对象语言。Java/C++的面向对象是object - class的关系,而JS是object - object的关系,中间通过原型prototype连接,父类和子类形成一条原型链。本文通过分析JS的对象的封装,再探讨正确实现继承的方式,然后讨论几个问题,最后再对ES6新引入的类class关键字作一个简单的说明。

JS的类其实是一个函数function,由于不是典型的OOP的类,因此也叫伪类。理解JS的类,需要对JS里的function有一个比较好的认识。首先,function本身就是一个object,可以当作函数的参数,也可以当作返回值,跟普通的object无异。然后function可以当作一个类来使用,例如要实现一个String类

1 var MyString = function(str){
2     this.content = str;
3 };
4
5 var name = new MyString("hanMeimei");
6 var addr = new MyString("China");
7 console.log(name.content + " live in " + addr.content);

第一行声明了一个MyString的函数,得到一个MyString类,同时这个函数也是MyString的构造函数。第5行new一个对象,会去执行构造函数,this指向新产生的对象,第2行给这个对象添加一个content的属性,然后将新对象的地址赋值给name。第6行又去新建一object,注意这里的this指向了新的对象,因此新产生的content和前面是不一样的。

上面的代码在浏览器运行有一点问题,因为这段代码是在全局作用域下运行,定义的name变量也是全局的,因此实际上执行var name = new MyString("")等同于window.name = new MyString(""),由于name是window已经存在的一个变量,作为window.open的第二个参数,可用来跨域的时候传数据。但由于window.name不支持设置成自定义函数的实例,因此设置无效,还是保持默认值:值为"[object Object]"的String。解决办法是把代码的运行环境改成局部的,也就是说用一个function包起来:

(function(){
    var name = new MyString("hanMeimei");
    console.log(name.content); //正确,输出hanMeimei
})(); 

所以从此处看到,代码用一个function包起来,不去污染全局作用域,还是挺有必要的。接下来,回到正题。

JS里的每一个function都有一个prototype属性,这个属性指向一个普通的object,即存放了这个object的地址。这个function new出来的每个实例都会被带上一个指针(通常为__proto__)指向prototype指向的那个object。其过程类似于:

var name = new MyString();             //产生一个对象,执行构造函数
name.__proto__ = MyString.prototype;   //添加一个__proto__属性,指向类的prototype(这行代码仅为说明)

如下图所示,name和addr的__proto__指向MyString的prototype对象:

可以看出在JS里,将类的方法放在function的prototype里面,它的每个实例都将获得类的方法。

现在为MyString添加一个toString的方法:

MyString.prototype.toString = function(){
    return this.content;
};

MyString的prototype对象(object)将会添加一个新的属性。

这个时候实例name和addr就拥有了这个方法,调用这个方法:

console.log(name.toString()); //输出hanMeimei
console.log(name + " lives in " + addr); //“+”连接字符时,自动调用toString,输出hanMeimei lives in China

这样就实现了基本的封装——类的属性在构造函数里定义,如MyString的content;而类的方法在函数的prototype里添加,如MyString的toString方法。

这个时候,考虑一个基础的问题,为什么在原型上添加的方法就可以被类的对象引用到呢?因为JS首先会在该对象上查找该方法,如果没有找到就会去它的原型上查找。例如执行name.toString(),第一步name这个object本身没有toString(只有一个content属性),于是向name的原型对象查找,即__proto__指向的那个object,发现有toString这个属性,因此就找到了。

要是没有为MyString添加toString方法呢?由于MyString实际上是一个Function对象,上面定义MyString语法作用等效于:

//只是为了示例,应避免使用这种语法形式,因为会导致两次编译,影响效率var MyString = new Function("str", "this.content = str");  

通过比较MyString和Function的__proto__,可以从侧面看出MyString其实是Function的一个实例:

console.log(MyString.__proto__); //输出[Function: Empty]
console.log(Function.__proto__); //输出[Function: Empty]

MyString的__proto__的指针,指向Function的prototype,通过浏览器的调试功能,可以看到,这个原型就是Object的原型,如下图所示:

因为Object是JS里面的根类,所有其它的类都继承于它,这个根类提供了toString、valueOf等6个方法。

因此,找到了Object原型的toString方法,查找完成并执行:

console.log(name.toString()); //输出{ content: ‘hanMeimei‘ }

到这里可以看到,JS里的继承就是让function(如MyString)的原型的__proto__指向另一个function(如Object)的原型。基于此,写一个自定义的类UnicodeString继承于MyString

var UString = function(){ };

实现继承:

UString.prototype = MyString.prototype; //错误实现

注意上面的继承方法是错误的,这样只是简单的将UString的原型指向了MyString的原型,即UString和MyString使用了相同的原型,子类UString增删改原型的方法,MyString也会相应地变化,另外一个继承MyString如AsciiString的类也会相应地变化。依照上文分析,应该是让UString的原型里的的__proto__属性指向MyString的原型,而不是让UString的原型指向MyString。也就是说,得让UString有自己的独立的原型,在它的原型上添加一个指针指向父类的原型:

UString.prototype.__proto__ = MyString.prototype;  //不是正确的实现

因为__proto__不是一个标准的语法,在有些浏览器上是不可见的,如果在Firefox上运行上面这段代码,Firefox会给出警告:

mutating the [[Prototype]] of an object will cause your code to run very slowly; instead create the object with the correct initial [[Prototype]] value using Object.create

合理的做法应该是让prototype等于一个object,这个object的__proto__指向父类的原型,因此这个object须要是一个function的实例,而这个function的prototype指向父类的原型,所以得出以下实现:

1 Object.create = function(o){
2     var F = function(){};
3     F.prototype = o;
4     return new F();
5 };
6
7 UString.prototype = Object.create(MyString.prototype);

代码第2行,定义一个临时的function,第3行让这个function的原型指向父类的原型,第4行返回一个实例,这个实例的__proto__就指向了父类的prototype,第7行再把这个实例赋值给子类的prototype。继承的实现到这里基本上就完成了。

但是还有一个小问题。正常的prototype里面会有一个constructor指向构造函数function本身,例如上面的MyString:

这个constructor的作用就在于,可在原型里面调用构造函数,例如给MyString类增加一个copy拷贝函数:

1 MyString.prototype.copy = function(){
2 //  return MyString(this.content);              //这样实现有问题,下面再作分析
3     return new this.constructor(this.content);  //正确实现
4 };
5
6 var anotherName = name.copy();
7 console.log(anotherName.toString());            //输出hanMeimei
8 console.log(anotherName instanceof MyString);   //输出true

问题就于:Object.create的那段代码里第7行,完全覆盖掉了UString的prototype,取代的是一个新的object,这个object的__proto__指向父类即MyString的原型,因此UString.prototype.constructor在查找的时候,UString.prototype没有constructor这个属性,于是向它指向的__proto__查找,找到了MyString的constructor,因此UString的constructor实际上是MyString的constuctor,如下所示,ustr2实际上是MyString的实例,而不是期望的UString。而不用constructor,直接使用名字进行调用(上面代码第2行)也会有这个问题。

var ustr = new UString();
var ustr2 = ustr.copy();
console.log(ustr  instanceof UString); //输出true
console.log(ustr2 instanceof UString); //输出false
console.log(ustr2 instanceof Mystring); //输出true

所以实现继承后需要加多一步操作,将子类UString原型里的constructor指回它自己:

UString.prototype.constructor = UString;

在执行copy函数里的this.constructor()时,实际上就是UString()。这时候再做instanseof判断就正常了:

console.log(ustr2 instanceof Ustring); //输出true

可以把相关操作封装成一个函数,方便复用。

基本的继承核心的地方到这里就结束了,接下来还有几个问题需要考虑。

第一个是子类构造函数里如何调用父类的构造函数,直接把父类的构造函数当作一个普通的函数用,同时传一个子类的this指针:

1 var UString = function(str){
2 // MyString(str);   //不正确的实现
3     MyString.call(this, str);
4 };
5
6 var ustr = new UString("hanMeimei");
7 console.log(ustr + "");  //输出hanMeimei

注意第3行传了一个this指针,在调用MyString的时候,这个this就指向了新产生的UString对象,如果直接使用第2行,那么执行的上下文是window,this将会指向window,this.content = str等价于window.content = str。

第二个问题是私有属性的实现,在最开始的构造函数里定义的变量,其实例是公有的,可以直接访问,如下:

var MyString = function(str){
    this.content = str;
};

var str = new MyString("hello");
console.log(str.content);        //直接访问,输出hello

但是典型的面向对象编程里,属性应该是私有的,操作属性应该通过类提供的方法/接口进行访问,这样才能达到封装的目的。在JS里面要实现私有,得借助function的作用域:

var MyString = function(str){
    this.sayHi = function(){
        return "hi " + str;
    }
};

var str = new MyString("hanMeimei");
console.log(str.sayHi());            //输出hi, hanMeimei

但是这样的一个问题是,必须将函数的定义放在构造函数里,而不是之前讨论的原型,导致每生成一个实例,就会给这个实例添加一个一模一样的函数,造成内存空间的浪费。所以这样的实现是内存为代价的。如果产生很多实例,内存空间会大幅增加,这个问题是不可忽略的,因此在JS里面实现属性私有不太现实,即使在ES6的class语法也没有实现。但是可以给类添加静态的私有成员变量,这个私有的变量为类的所有实例所共享,如下面的案例:

var Worker;
(function(){
    var id = 1000;
    Worker = function(){
        id++;
    };
    Worker.prototype.getId = function(){
        return id;
    };
})();

var worker1 = new Worker();
console.log(worker1.getId());   //输出1001
var worker2 = new Worker();
console.log(worker2.getId());   //输出1002

上面的例子使用了类的静态变量,给每个worker产生唯一的id。同时这个id是不允许worker实例直接修改的。

第三个问题是虚函数,在JS里面讨论虚函数是没有太大的意义的。虚函数的一个很大的作用是实现运行时的动态,这个运行时的动态是根据子类的类型决定的,但是JS是一种弱类型的语言,子类的类型都是var,只要子类有相应的方法,就可以传参“多态”运行了。比强类型的语言如C++/Java作了很大的简化。

最后再简单说下ES6新引入的class关键字

 1 //需要在strict模式运行
 2 ‘use strict‘;
 3 class MyString{
 4     constructor(str){
 5         this.content = str;
 6     }
 7     toString(){
 8         return this.content;
 9     }
10     //添加了static静态函数关键字
11     static concat(str1, str2){
12         return str1 + str2;
13     }
14 }
15
16 //extends继承关键字
17 class UString extends MyString{
18     constructor(str){
19     //使用super调用父类的方法
20         super(str);
21     }
22 }
23
24 var str1 = new MyString("hello"),
25     str2 = new MyString(" world");
26 console.log(str1);                       //输出MyString {content: "hello"}
27 console.log(str1.content);               //输出hello
28 console.log(str1.toString());            //输出hello
29 console.log(MyString.concat(str1, str2));//输出hello world30
31 var ustr = new UString("ustring");
32 console.log(ustr);                       //输出MyString {content: "ustring"}
33 console.log(ustr.toString());            //输出ustring

从输出的结果来看,新的class还是没有实现属性私有的功能,见第27行。并且从第26行看出,所谓的class其实就是编译器帮我们实现了上面复杂的过程,其本质是一样的,但是让代码变得更加简化明了。一个不同点是,多了static关键字,直接用类名调用类的函数。ES6的支持度还不高,最新的chrome和safari已经支持class,firefox的支持性还不太好。

最后,虽然一般的网页的JS很多都是小工程,看似不需要封装、继承这些技术,但是如果如果能够用面向对象的思想编写代码,不管工程大小,只要应用得当,甚至结合一些设计模式的思想,会让代码的可维护性和扩展性更高。所以平时可以尝试着这样写。

时间: 2024-10-09 12:33:17

深入浅出JS的封装与继承的相关文章

JS之封装、继承、多态

JS是一种十分灵活的语言,不谈多态(或者说它本身就是多态的)封装概念: 封闭一部分,外界无法直接访问 通过开放部分间接访问私有部分例子: 不封装:构造函数的所有属性都是开放的 function Girl(name,bf){ this.name = name; this.bf = bf; } var girl = new Girl("林黛玉","贾宝玉"); alert(girl.name + "love" + girl.bf);//林黛玉love

JS面向对象编程之:封装、继承、多态

最近在实习公司写代码,被隔壁的哥们吐槽说,代码写的没有一点艺术.为了让我的代码多点艺术,我就重新温故了<javascript高级程序设计>(其中几章),然后又看了<javascript设计模式>,然后觉得要写点心得体会,来整理自己所学的吧.以下是我个人见解,错了请轻喷,欢迎指出错误,乐于改正. 一.封装 (1)封装通俗的说,就是我有一些秘密不想让人知道,就通过私有化变量和私有化方法,这样外界就访问不到了.然后如果你有一些很想让大家知道的东西,你就可以通过this创建的属性看作是对象

深入浅出js事件

深入浅出js事件 一.事件流 事件冒泡和事件捕获分别由微软和网景公司提出,这两个概念是为了解决页面中事件流(事件发生顺序)的问题. <div id="outer"> <p id="inner">Click me!</p> </div> 上面的代码当中一个div元素当中有一个p子元素,如果两个元素都有一个click的处理函数,那么我们怎么才能知道哪一个函数会首先被触发呢? 为了解决这个问题微软和网景提出了两种几乎完全相反

2、C#面向对象:封装、继承、多态、String、集合、文件(上)

面向对象封装 一.面向对象概念 面向过程:面向的是完成一件事情的过程,强调的是完成这件事情的动作. 面向对象:找个对象帮你完成这件事情. 二.面向对象封装 把方法进行封装,隐藏实现细节,外部直接调用. 打包,便于管理,为了解决大型项目的维护与管理. 三.什么是类? 将相同的属性和相同方法的对象进行封装,抽象出 “类”,用来确定对象具有的属性和方法. 类.对象关系:人是类,张三是人类的对象. 类是抽象的,对象是具体的.对象可以叫做类的实例,类是不站内存的,对象才占内存. 字段是类的状态,方法是类执

js的6种继承方式

重新理解js的6种继承方式 注:本文引用于http://www.cnblogs.com/ayqy/p/4471638.html 重点看第三点 组合继承(最常用) 写在前面 一直不喜欢JS的OOP,在学习阶段好像也用不到,总觉得JS的OOP不伦不类的,可能是因为先接触了Java,所以对JS的OO部分有些抵触. 偏见归偏见,既然面试官问到了JS的OOP,那么说明这东西肯定是有用的,应该抛开偏见,认真地了解一下 约定 P.S.下面将展开一个有点长的故事,所以有必要提前约定共同语言: 1 2 3 4 5

深入理解JS原型链与继承

我 觉得阅读精彩的文章是提升自己最快的方法,而且我发现人在不同阶段看待同样的东西都会有不同的收获,有一天你看到一本好书或者好的文章,请记得收藏起来, 隔断时间再去看看,我想应该会有很大的收获.其实今天要讨论的主题,有许多人写过许多精彩的文章,但是今天我还是想把自己的理解的知识记录下来.我在学习 掌握JS原型链和继承的时候,就是看得@阮一峰老师的写的文章,觉得他写的技术类的文章都容易让理解,简明概要,又好理解.他是我学习JS路程里面一个比较佩服的导师,昨天重新看了他写的<Javascript 面向

浅谈JavaScript的面向对象和它的封装、继承、多态

写在前面 既然是浅谈,就不会从原理上深度分析,只是帮助我们更好地理解... 面向对象与面向过程 面向对象和面向过程是两种不同的编程思想,刚开始接触编程的时候,我们大都是从面向过程起步的,毕竟像我一样,大家接触的第一门计算机语言大概率都是C语言,C语言就是一门典型的面向过程的计算机语言.面向过程主要是以动词为主,解决问题的方式是按照顺序一步一步调用不同的函数.面向对象是以名词为主,将问题抽象出具体的对象,而这个对象有自己的属性和方法,在解决问题的时候,是将不同的对象组合在一起使用. //面向过程装

封装、继承、多态——简介

封装 封装将属性或方法隐藏,对外开放接口. 在写项目的时候,我们经常会在不同的地方需要用到相同的方法或属性,倘若每个地方都要写一遍,不仅代码量大而且也会浪费我们的时间,这时候我们就可以将这些方法或属性封装起来 // 定义一个运算的函数 class Operation{ constructor(){} // 写一个加法函数 add(x, y){ return x + y } // 写一个减法函数 subtraction(x, y){ return x - y } } // 需要的时候,我们只需要实

怎么理解js的原型链继承?

前言 了解java等面向对象语言的童鞋应该知道.面向对象的三大特性就是:封装,继承,多态. 今天,我们就来聊一聊继承.但是,注意,我们现在说的是js的继承. 在js的es6语法出来之前,我们想实现js的继承关系,需要借助于原型链.之前的文章,我有讲过原型和原型链的概念.在这,再重新回顾一下. js中万物皆对象,每个对象都有一个隐式原型 __proto__ ,指向创建它的构造函数的原型对象. 函数(构造函数)除了有一个隐式原型对象,还有一个属性prototype,它指向一个对象,这个对象就是原型对