什么是javascript封装,封装的方法有几种
发布时间:2025-05-21 00:22:58 发布人:远客网络
一、什么是javascript封装,封装的方法有几种
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());//输出hanMeimeiconsole.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
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());//输出hanMeimei8 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);//输出trueconsole.log(ustr2 instanceof UString);//输出falseconsole.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语法也没有实现。但是可以给类添加静态的私有成员变量,这个私有的变量为类的所有实例所共享,如下面的案例:
Worker.prototype.getId= function(){ return id;
})();var worker1= new Worker();
console.log(worker1.getId());//输出1001var 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);//输出hello28 console.log(str1.toString());//输出hello29 console.log(MyString.concat(str1, str2));//输出hello world
3031 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很多都是小工程,看似不需要封装、继承这些技术,但是如果如果能够用面向对象的思想编写代码,不管工程大小,只要应用得当,甚至结合一些设计模式的思想,会让代码的可维护性和扩展性更高。所以平时可以尝试着这样写。
二、如何系统地学习 JavaScript
1、自从AJAX开始流行后,人们发现利用JavaScript可以给用户带来更好的体验,甚至利用这一优点开发了大型网页游戏,于是这门小语言被重视了起来。现在,很多公司会招专门的JavaScript工程师,通常JavaScript是WEB前端开发的必备技能。简单介绍了JavaScriptr的好处,并不代表大家就会去学习甚至把它学好,兴趣是很关键的,我认为兴趣是最好的老师,它是你专心做一件事并把它做好的动力。另外,大家要相信小语言有大作为,我就曾用Greasemonkey写过一些非常实用的工具,比如你可以用JavaScript+Greasemonkey写在线网页游戏的外挂程序。下面说说本人学习JavaScript的历程和心得吧。
2、使用DIV+CSS布局标准网页,可以使前端XHTML代码更少、结构更清晰,这有利于轻松用JavaScript操作DOM,比如,要展示一个3行3列的列表,如果用传统的表格布局,现在要你用JavaScript动态生成这个列表,那么就需要一个循环嵌套,如果采用li结构加CSS浮动布局,一次循环就好了。当然,WEB标准化不是一定不能使用表格,我的意思是结构清晰的XHTML更易于把JavaScript效果或功能整合到项目中。
3、作为一个开发人员,熟悉测试工具是必须的,这有助于提高你发现问题和解决问题的效率,对于特别大的项目更是如此。JavaScript和XHTML开发测试利器我就先推荐两个最常用的,它们是:Web Developer和Firebug。
4、熟悉JavaScript每一个方法的作用
5、这一要求听起来似乎有点不太实际,我想这个要求对于像C#、JAVA这些大型语言来说确实是,因为这些语言类库实在太庞大了,相信没有人可以全面记住它,而且也是没有必要全部记住,比如用JAVA做网页与手机开发所关注的类库是不一样的。而JavaScript则不是,它的内置方法函数真的不多,先全面熟悉一下,开发起来也将得心应手,比如,你一开始可能认为JavaScript有trim()这个很多语言都有的去行头行尾空格的方法,当你了解JavaScript内置函数库后你会发现原来在JavaScript中这些方法是要自己去实现。再比如,如果你是从其它比较强大的语言转过来玩JavaScript,你又可能认为JavaScript应该有MD5加密的方法,当然这也是没有的,但有人用JavaScript实现了这样的方法,即JavaScript MD5。说到底JavaScript内置方法少的可怜,但很多牛人写了一些新方法增加JavaScript功能,比如prototype框架主要是对JavaScript基础函数进行原型扩展的。
6、了解DOM编程算是学习JavaScript过程比较重要的课程,因为JavaScript除了编写一些纯数据处理的逻辑外,更多的是在动态更改XHTML的结构和内容,以达到界面动态更新的目的,而这些工作都要依赖DOM编程。jQuery框架在这方面封装的相当好,提供了丰富的DOM操作方法,可以让你轻松找到页面任何地方的一个DOM节点(XHTML标签),然后进行相关操作(增、删、改、查)。对于有过用其它语言操作XML文档经验的朋友,相信这一块很快上手。
7、在今天,学习了JavaScript而不使用AJAX,那是埋没JavaScript优势了(AJAX本身并不能算是JavaScript内容)。AJAX对于用户以及服务器来说都是有好处的,对于用户,提供更好的用户体验,最典型的一个应用场景:注册页面的用户名可用性预检测,传统的可能会遇到这样的问题:用户填写了一堆资料后提交表单,结果被服务器告知这个用户被注册了,要用户重新填写资料注册,这对于大型多用户网站那是很致命的,因为用户输10个用户名可能有一半已被使用了。对于服务器来说,减少网页流量,因为AJAX后,一般是按需加载数据的,不会因为局部更新而重新加载整个页面。比如一个网页占三屏高,我们可以默认只加载第一屏的内容,当用户拉动滚动条往下的时候,再加载二三屏的内容。另外像WEB在线地图应用也是AJAX使用的典范。
8、提高深入理解JavaScript this关键字
9、JavaScript this的作用与指向跟很多真正面向对象的开发语言是有很大差别的,它不始终指向当前对象,是会变化的。如果不注意这个问题,就有可能遇到看似没错的代码报错或不执行等问题。
10、OOP是个好东西,它使程序员思考问题更有组织性,代码的组织也更清晰。JavaScript也可以OOP,但与一些传统的OOP语言同样存在较大差别,所以要真正用熟JavaScript OOP也是需要下点功夫的。
11、了解闭包JavaScript闭包也是被谈得比较多的一个话题,闭包使JavaScript变量作用域变得复杂起来,但这一特性又使这门语言变的更灵活了。
12、团队精神相信大家是听过不少了,就职一个公司做开发,当然离不开团队,团队要想合作愉快,每个成员的编码必需符合一定规范,这也是每个公司对程序员的基本要求。关于规范通常指变量命名、文件组织、注释规范等,这方面知识与其它语言的规范是有相通性的。
13、我认为学会并使用一门语言并不是什么难事,难的应该是用语言这基本的语法与语句去解决一些复杂的问题。要解决一些复杂的问题,可能会用上一些算法,有些算法实现可能是一个团队在做的,比如中科院中文分词的具体实现,另外像游戏常用自动寻径A*算法等。也许你认为你不可能在JavaScript开发上遇到这么复杂的问题,其实这还要看你在做什么项目,如果你是在开发大型网页游戏,通常面临更多具有挑战性的难题,如果你有读过一些算法书籍,了解一些常见问题的解决方法,在开发过程中自然会如虎添翼。另外,熟悉算法显然对于你日后接触其它开发语言也是有帮助的。
14、CSS与JavaScript浏览器兼容问题最好做好笔记,因为这些问题,或者说BUG是比较诡异的,这些问题浏览器不会提示你错在哪里,IDE也不会提示你,特别是CSS兼容问题,这就会导致你在开发过程碰到这样的问题会卡老半天得不到解决。即使之前解决过同类问题,以后可能又会碰上,结果之前没有做好笔记加强记忆,注定你要再受罪一次,因为这些兼容问题不是一两个,临时记忆效果是不明显的。以我经验,很多问题是在IE6下发生的,现在IE6连微软自己都希望加速它灭亡,这对于前端开发的我们来说当然是一个好消息。
15、阅读优秀代码绝对也是自我提高的好方法,这不仅可以了解优秀代码的组织规范,更可以了解一些功能的实现思路。比如jQuery就是一个非常值的学习的JavaScript框架。当然了,要阅读这样专业的JavaScript框架,JavaScript基础要扎实,不然看的过程中会遇到太多疑问,甚至对自己的信心也是一种打击。
16、注意在JavaScript入门之前避免直接使用JavaScript框架做开发
17、如果你没有任何JavaScript基础,请不要直接使用JavaScript框架做开发,我认为这可能会误导你对一门语言的认识,比如你直接使用了jQuery用点连起来的语句写法,你是否会认为这是JavaScript语法的一种呢?再比如你使了prototype你不要把框架扩展后的基类方法认为是JavaScript内置的。我认为JavaScript框架是用来提高效率的,它绝对不是JavaScript入门应该学习的。
18、网上找的很多例子可能不是跨浏览器兼容的
19、有很多JavaScript效果源码是N年前某网友写的,N年前是IE的天下,于是一些前端懒得解决脚本跨浏览器兼容问题,使写出来的脚本只适用于IE。
三、JS创建对象几种不同方法详解_javascript技巧
本文介绍了几种js创建对象的方法,分享给大家供大家参考,具体内容如下
弊端:没有解决对象的识别问题,即怎么知道一个对象的类型。
2、直接将属性和方法赋给了this对象
要创建person的实例,必须使用new操作符,以这种方式调用构造函数实际上会经历4个步骤:
2、将构造函数的作用域赋给新对象
创建自定义的构造函数可以将它的实例标识为一种特定的类型。
每个方法都有在每个实例上重新创建一遍。person1和person2都有一个sayName()的方法,但两个方法不是同一个Function实例。不同实例上的同名函数是不相等的。
创建两个完成同样任务的Function实例没有必要,而且还有this对象在,不需要在执行代码前就把函数绑定在特定对象上,可以像下面这样。
把sayName属性设置成全局的sayName函数,这样,由于sayName包含的是一个指向函数的指针,因此person1和person2对象就共享了同一个函数。
但是,如果对象需要定义很多方法,那么就要定义很多全局函数,自定义的引用类型也没有封装可言了。为了解决上述问题,引入原型模式。
我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。prototype是通过调用构造函数而创建的那个对象实例的对象原型,使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。
首先,解析器会问实例person1是否有name属性,如果有,就返回。
如果没有,就继续去person1的原型中搜索name属性,如果有就返回。
如果没有,再继续向person1的原型的原型中搜索。
isPrototypeOf()确定实例和原型对象之间的关联
console.log(Person.prototype.isPrototypeOf(person1));//true
Object.getPrototypeOf()返回的是[[prototype]]的值
console.log(Object.getPrototypeOf(person1));
//Person{name:“Yvette”, age: 26, job:“engineer”}返回的是Person的原型对象。
console.log(Object.getPrototypeOf(person1)=== Person.prototype)//true
console.log(Object.getPrototypeOf(person1).name);//”Yvette”
hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中,只有给定属性存在于实例中,才会返回true。
console.log(person1.hasOwnProperty(“name”));//false
有两种方式使用in操作符:单独使用和在for-in循环中使用。单独使用时,in操作符会在通过对象能够访问给定属性时返回true,无论该属性在于实例中还是原型中。
使用for in循环,返回的是所有能够通过对象访问的、可枚举的属性,其中既包括实例中的属性,也包括存在于原型中的属性。如果实例中的属性屏蔽了原型中不可枚举的属性,那么也会返回。IE9之前的版本实现上有一个Bug,屏蔽不可枚举属性的实例属性不会在for-in中返回。
在IE9之前的吧按本中没有log信息。尽管person实例中的toString()方法屏蔽了原型中的不可枚举的toString();
这导致了person1.constructor不再指向Person,而是指向了Object。如果constructor很重要,则需要特意将其设为适当的值,如:
但是这种方式会导致constructor属性变成可枚举。
如果想设置为不可枚举的(默认不可枚举),可以使用Object.defineProperty(Person.prototype,“constructor”,{
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来。
如果重写整个原型对象,情况就不一样了。调用构造函数时会为实例添加一个指向最初原型的[[prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。实例中的指针仅指向原型,而不指向构造函数。
person.prototype指向的是原本的原型对象,而不会指向新的原型对象。
原型模式最大问题是由其共享的本性所导致的。
对于包含引用类型值的属性来说,问题较为突出
本意只想修改person1的friends,但是却导致person2的friends属性值也改变了。因此我们很少单独使用原型模式。
创建自定义类型的最常用的方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性,这样每个实例都有自己的一份实例属性的副本,又同时共享着对方法的引用,最大限度的节省了内存。