面向对象JavaScript开发实战

来源:互联网 发布:域名交易网站有哪些 编辑:程序博客网 时间:2024/06/05 20:55

面向对象JavaScript开发实战


一. JavaScript高级部分

1.1 函数

1.1.1 函数的内存分布

        JavaScript中定义的每个函数在被解析的时候都将分配一个prototype属性,该prototype引用一个对象,而这个对象的constructor属性又引用到原函数,如下代码所示:
function Person(_name) {this.name = _name;this.sayHi = function() {console.log('hi, I am '+this.name);};}Person.prototype.age = 25;Person.prototype.get = function(properName) {return this[properName];};


        函数的prototype对象可以对函数进行方法的扩展,其用法非常灵活。由于JavaScript语言缺乏很多静态语言所拥有的特性,如对象类、继承和多态等,要想在JavaScript中模拟实现这些特性,prototype对象起着非常关键的作用,在后面章节中将会详细提及。

1.1.2 函数的声明方式

        函数的声明有如下几种方式。
方式一:new Function(arg1,arg2,arg3,body) 
var func = new Function("name","age", "console.log('name:'+name+', age:'+age);");func('Peter', 23);
注意:函数也是对象,由Function实例化的,通常,可以通过在Function.prototype对象上定义通用方法,使得所有函数均持有该方法。

方式二:var func = function() {}
var func = function(name, age) {console.log('name:'+name+', age:'+age);};func('Peter', 23);
方式三:function func() {} 
function func(name, age) {console.log('name:'+name+', age:'+age);}func('Peter', 23);

1.1.3 函数的调用方式

        函数被调用的方式有多种,不同的调用方式适合不同的场景。JavaScript最常见的函数调用方式有如下几种:
方式一,作为函数直接调用
function hello() {console.log('hello, world');}hello();
方式二,作为对象的构造函数调用
function Hello() {console.log('create a Hello object.');}var hello = new Hello();
方式三,作为对象方法调用
var peter = {name: 'peter',age: 25,showProfile: function() {console.log('name:'+this.name+', age:'+this.age);}};peter.showProfile();
方式四,采用call或者apply进行调用,用法如下:
method.call(obj, arg1, arg2…);
method.apply(obj, [arg1, arg2, arg3]);
var showProfile = function(other) {console.log('name:'+this.name+', age:'+this.age+', other:'+other);};var peter = {name: 'peter',age: 25};showProfile.call(peter, 'call');showProfile.apply(peter, ['apply']);
运行结果:
name:peter, age:25, other:callname:peter, age:25, other:apply
       由此可见,JavaScript函数能够以非常灵活的方式使用,而正因为这种灵活性导致开发人员不易于掌握他们,这要求开发人员要经常实践才能深刻理解他们的精妙。

1.1.4 this的用法

        JavaScript中this的用法非常灵活,也是非常难以熟练掌握的。由于其运行期绑定的特性,this含义非常丰富,它可以是全局对象、当前对象或者任意对象,这完全取决于函数的调用方式。JavaScript 中函数的调用有以下几种方式:作为对象方法调用,作为函数调用,作为构造函数调用和使用 apply 或 call 调用。下面我们将按照调用方式的不同,分别讨论 this 的含义。

当作为对象方法被调用的时候,this指的是该对象本身,见下面例子:
var point = {x: 0,y: 0,move: function(witdh, heigh) {this.x = this.x + width;this.y = this.y + height;}};point.move(5, 10);//this绑定到当前对象,即point对象
当作为函数直接被调用时,this指的是当前上下文:
function makeNoSense(x) {this.x = x;}makeNoSense(5);//相当于window.makeNoSense(5);console.log(x);//输出为:5
当作为构造方法调用的时候,this绑定到新创建的对象上:
function Point(x, y) {this.x = x;this.y = y;}var point = new Point(10, 20);console.log(point.x);//输出:10console.log(point.y);//输出:20
当用call或apply方式调用时,this绑定到第一个参数对象:
function Point(x, y) {this.x = x;this.y = y;this.moveTo = function(x, y) {this.x = x;this.y = y;};}var p1 = new Point(0, 0);var p2 = {x: 0, y: 0};p1.moveTo(1, 1);p1.moveTo.apply(p2, [10, 10]);//this绑定到p2对象console.log(p1.x+', '+p1.y);//输出:1, 1console.log(p2.x+', '+p2.y);//输出:10, 10
最后来看一个jQuery绑定事件的例子
HTML代码
<div class="ui-btn ui-active" data-action="showPageFromMenu" data-rel="pageHome"><img class="ui-btn-tit-pic" src="img/home.png"/>首页</div><div class="ui-btn" data-action="showPageFromMenu" data-rel="pageNotice"><img class="ui-btn-tit-pic" src="img/message.png"/>业务通知</div><div class="ui-btn" data-action="showPageFromMenu" data-rel="pageArticle"><img class="ui-btn-tit-pic" src="img/book.png"/>最新发文</div>
JavaScript代码
$('body').on('tap click', '.ui-btn', function() {//this指的是触发事件的dom元素var action = $(this).attr('data-action');app[action](this);});
        这块代码的目的是要在具有相同class样式的div上面注册click事件,其传入的回调函数最终将会这样被调用:div.onclick = callback;,因此,回调函数中的this便是指被触发事件的div元素,总而言之一句话:this指的是函数的调用者

1.1.5 callback回调函数

        JavaScript回调函数是指将一个函数作为参数传入到另一个函数(主函数),该函数将在主函数内有机会被执行,这样函数称之为回调函数(callback)。callback可以用来模拟实现静态语言中的多态特性,也可以用来实现异步功能。常见的使用到回调函数的情形有setTimeout、Ajax和Event等。
$.ajax({url: 'test.html',context: document.body}).done(function() {$(this).addClass('done');}).fail(function() {alert('error');}).always(function() {alert('complete');});

1.2 对象

1.2.1 对象的内存分布

        对象定义:属性的无序集合,每个属性存放一个原始值、对象或函数。每个对象都是由类定义的,通过类实例化对象,在JavaScript中并没有正式的类,开发人员通过函数作为类来定义对象的属性和方法,与其他静态语言中的类的概念相比,两者是等价的。

1.2.2 对象的创建

       我们可以通过以下两种方式创建一个对象:字面量和构造函数。这两种方式分别用于不同的场景。
字面量创建对象
var obj = {attr1: 'attr1',attr2: 20,print: function() {console.log('attr1:'+this.attr1+', attr2:'+this.attr2);}};
        这种方式适合于封装复杂数据结构,创建普通对象,并且将该对象作为全局变量使用,由于这种方式创建对象非常简单明了,是比较常见的创建对象的方式。
构造函数创建对象
function Person() {this.name = 'Peter';this.sayHello = function() {console.log('hello, '+this.name);};}var person = new Person();person.sayHello();//输出:hello, Peter

        通过构造函数创建对象,这种用关键字new的方式跟其他静态语言几乎一样,只不过JavaScript没有原生的对类继承的支持,因此需要我们模拟实现类继承的特性,这将在后面介绍。

        构造函数创建对象适合于需要按照类创建多个实例的场景,在这种场景下,每个对象实例都拥有自己独立的属性和方法,为了节省更多的资源,我们往往把对象方法提取到类的prototype中去,这样多个对象实例便可以重用同一个方法,这也是我们常常所推荐的用来定义对象的方式。
function Person(name) {this.name = name;}Person.prototype.sayHello = function() {console.log('hello, '+this.name);};var p1 = new Person('Peter');p1.sayHello();//输出:hello, Petervar p2 = new Person('Hans');p2.sayHello();//输出:hello, Hans


1.3 事件

1.3.1 JavaScript单线程

        JavaScript是单线程的,因此当一个程序需要处理多个任务的时候,程序只能在前一个任务执行完毕后才开始执行后一个任务,如果前面的任务执行需要花费很长时间,这将导致后面的任务阻塞和浏览器假死,为了避免这种情况,我们需要应用异步模式对程序进行优化,将耗时比较多的任务放到事件队列,等到主体函数执行完后再执行事件队列里面的任务,常见的异步模式有:回调函数、事件监听、发布订阅和Promises对象等(相关知识点请上网查询)。

1.3.2 JavaScript事件机制

var startTime = new Date();setTimeout(function() {var endTime = new Date();console.log(endTime - startTime);}, 500);while(new Date() - startTime < 1000) {}
        以上代码执行的结果并不是预期的500毫秒,而是1000毫秒(可能存在一点误差),这是因为setTimeout参数中的回调函数并未在500毫秒后立即执行,而是被放到一个事件队列中去了,等到主体函数执行完毕之后才开始执行该回调函数。同样的,在DOM元素上触发事件也不是立即执行事件绑定的函数,而是等待DOM所有事件触发后才开始执行绑定到事件的函数。

下面的例子将使用setTimeout和callback来实现异步模式的处理方式。
function asyn(callback) {setTimeout(callback, 0);}function readFile() {var startTime = new Date();while(new Date() - startTime < 1000) {//read file will cost 1 second}console.log('finished reading file.');}console.log('start...');asyn(readFile);console.log('finished...');
输出结果:
start...finished...finished reading file.
        上面的代码中假设读取文件将花费1秒钟时间,那么我们将读取文件的操作放到事件队列中,等到主体函数执行完毕之后再执行文件读取操作,这样便可以防止程序阻塞的问题。读者有兴趣可以研究一下为什么NodeJS并发处理能力要优于其他静态语言程序(如Java),以便更深入了解JavaScript单线程和事件队列机制。

1.4 继承

1.4.1 创建类

        在JavaScript语言中,函数可以作为类进行对象实例化,但是JavaScript中没有类继承的概念,类继承可以大大降低重复代码,子类继承父类后便拥有父类的方法和属性,我们只能通过编程的方法去模拟实现类继承,这样一来类的创建就不仅仅是定义一个函数那么简单了,创建类可以这样写:
var Parent = new Class();var Child = Parent.extend();//Child类继承Parent类var parent = new Parent();var child = new Child();

1.4.2 类继承

        继承即子类继承父类的属性和方法,通过前面的介绍可知,在定义类(函数)的时候,一个类方法既可以声明在类内部,也可以声明在prototype对象里面,那么要实现一个子类,就需要把父类和prototype的属性和方法复制到子类,这才是关键所在。前面已经提到可以通过编程的方式模拟实现类继承的特性,下面介绍几种具体方法。

方式一:prototype链接,即子类的prototype对象链接到父类的prototype对象,这样子类便拥有了父类的prototype所定义的方法:


方式二:类抄写,即子类抄写父类的属性和方法,其实就是在子类构造方法内调用父类的构造方法来实现的,如下示例:
//declare parent classfunction Parent(name) {this.name = name;this.showName = function() {console.log('name:'+this.name);};}Parent.extend = function() {var Child = function() {Parent.apply(this, arguments);//继承父类构造方法};var F = function() {this.constructor = Child;};F.prototype = Parent.prototype;Child.prototype = new F();//间接方法,防止污染父类prototype对象return Child;};Parent.prototype.sayHello = function() {console.log('hello, '+this.name);};var parent = new Parent('Peter');parent.showName();parent.sayHello();var Child = Parent.extend();var child = new Child('Hans');child.showName();//该方法继承Person函数内定义的方法child.sayHello();//该方法继承Person原型对象内定义的方法
输出结果:
name:Peterhello, Petername:Hanshello, Hans
        读者可以思考一下为什么上面要引用一个中间对象F,其实理由很简单,如果不建立一个中间对象F,让子类prototype直接引用父类的prototype,这样便可以通过修改子类prototype来修改父类prototype,显然是不符合面向对象程序设计原则,建立中间对象是为了防止子类污染父类。这里请读自行描画出上面Parent和Child的内存分布图,以加深对其的理解。

方式三:prototype属性复制,某些时候你需要扩展子类的属性和方法,你可以在创建子类的时候定义新的属性和方法,写法如下:
var Child = Parent.extend({age: 27,showAge: function() {console.log('age:'+this.age);}});
extend方法传入的参数即为子类扩展的属性和方法,具体实现如下代码:
function Parent(name){this.name = name;this.sayHi = function() {console.log('hello, '+this.name);};}Parent.extend = function(options) {var Child = function() {Parent.apply(this, arguments);};for(var prop in options) {Child.prototype[prop] = options[prop];}return Child;};var Child = Parent.extend({age: 27,showAge: function() {console.log('age:'+this.age);}});var child = new Child('bxiao');child.sayHi();child.showAge();
输出结果:
hello, bxiaoage:27

方式四:圣杯模式,以上的几种继承手段各有其用处,但是都不能单独使用,如果将上面介绍的几种方式合并起来使用,相互弥补各自的缺陷,则可以达到真正的继承效果,具体实现请参考下面代码:
var Class = function(){};Class.new = function() {var klass = function() {//约定所有函数均申明了init方法,用于初始化if(typeof this.init === 'function') {this.init.apply(this, arguments);}};klass.extend = function(options) {var Parent = this;var Child = function(){Parent.apply(this, arguments);//继承父类的属性和方法};var F = function(){this.constructor = Child;};F.prototype = Parent.prototype;Child.prototype = new F();//间接方法,防止父类被污染for(var prop in options){//扩展属性和方法Child.prototype[prop] = options[prop];}        Child.extend = klass.extend;//让子类可以继续扩展子类return Child;};return klass;}var Parent = Class.new();//创建Parent类Parent.prototype.init = function(name) {//定义初始化方法this.name = name;this.showName = function() {console.log('name:'+this.name);};};Parent.prototype.sayHello = function() {//定义方法console.log('hello, '+this.name);};var parent = new Parent('Peter');//实例化Parent类parent.sayHello();//输出:hello, Peterparent.showName();//输出:name:Petervar Child = Parent.extend({//创建Child类,继承Parent并扩展属性和方法age : 35,showAge : function() {console.log('age:'+this.age);}});var child = new Child('Hans');//实例化Child类child.sayHello();//输出:hello, Hanschild.showName();//输出:name:Hanschild.showAge();//输出:age:35
该继承模式综合了上述几种模式,已经较为健壮了,读者可以直接仿照该继承模式来编写自己项目中的框架代码,相信能够给项目带来许多改善。

1.4.3 对象继承

        由于JavaScript动态扩展的特性,可以直接给对象添加和删除属性,在很多情况下需要让一个对象拥有另一个对象的方法和属性,这种做法我们称之为对象继承,其实叫对象扩展更加贴切。ECMAScript5制定了一系列新API,其中一个创建扩展对象的API是Object.create(prototype, descriptors),可以用来扩展对象,这种方式原型链干净,但是浏览器支持有限制。在JQuery中扩展一个对象可以调用如下方法:

extend(dest,src1,src2,src3...srcN)

其中dest为目标对象,该方法将 src1、src2...对象的属性和方法逐一复制给dest对象。对象扩展已经被很多JavaScript库做得很好了,所以我们无须重复发明轮子,放心地使用这些工具库,将大大提高前端开发效率。

1.5 模块

1.5.1 命名空间

        命名空间可以用来防止函数命名冲突,大多数静态语言都支持命名空间,虽然JavaScript不支持,但我们可以通过编程的手段来模拟实现命名空间,见如下代码示例:
var cn = cn || {};cn.hunnu = cn.hunnu || {};cn.hunnu.edu = cn.hunnu.edu || {};var Class = cn.hunnu.edu.Class = function(){};

1.5.2 模块创建

        前面讲了命名空间可以防止函数命名冲突,那么模块化则可以防止全局变量冲突,模块化编程要求一个js文件为一个模块,程序模块化可以使得系统设计变得优良,使得代码管理和维护变得轻松。下面通过代码简单地演示如何创建和使用模块:

编写cn.hunnu.edu.util.js文件
(function() {var name = 'Peter';var sayHello = function() {console.log('hello, '+this.name);};this.cn = this.cn || {};this.cn.hunnu = this.cn.hunnu || {};this.cn.hunnu.edu = this.cn.hunnu.edu || {};this.cn.hunnu.edu.util = {name: name,sayHello: sayHello};}).call(global);//在NodeJS环境中传入global全局对象,如果在浏览器中运行则传入window对象

编写app.js文件
console.log(cn.hunnu.edu.util.name);//输出:Petercn.hunnu.edu.util.sayHello();//输出:hello, Peter

        上述代码为什么这么写,这样写有什么优点?由于这些涉及到JavaScript作用域和闭包相关问题,请读者自行研究相关知识点来加深理解。读者不妨参考一些主流的JavaScript框架,几乎都会采用上述的方式进行设计。

1.5.3 AMD和CMD

        前面简单介绍了模块创建的基本原理,在实际应用中要使用模块化编程还需要考虑许多问题,如模块书写规范、模块依赖、异步加载等等,要解决这一系列问题就需要有一个统一的规范,目前已形成的两大规范分别是CMD和AMD。

        CMD是"Common Module Definition"的缩写,该规范明确了模块的基本书写格式和基本交互规则,该规范是Sea.js推广过程中形成的。

        AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行,该规范是在推广RequireJS库形成的。下面将简单介绍一下RequireJS的用法。

        首先到相关网站下载最新的require.js和其他js库,然后搭建一个基本工程目录,如下图所示,js/app目录放置应用程序模块,js/lib目录放置第三方js库,js/main.js为程序初始化入口:

- www/

     - index.html

     - js/

          - app/

                - util.js

                - a.js

          - lib/

                - jquery.js

                - backbone.js

          - main.js

在index.html文件中添加:

<script data-main="js/main" src="js/require.js"></script>
data-main属性告诉require.js在加载完require.js之后再加载js/main.js,因此一般把main.js作为程序入口,对整个程序进行初始化操作。

编辑main.js,添加如下内容:

requirejs.config({    baseUrl: 'js/lib',//定义模块根路径    paths: {        app: '../app'//定义模块子路径,相对于根路径    }});//开始主函数逻辑requirejs(['jquery', 'backbone', 'app/util', 'js/app/a.js'],     function ($,  backbone, util) {        //jQuery, backbone 和app/util模块都已全部加载        //并且可以直接在这里使用});

在util.js中定义模块:

/** * 定义模块 * define函数第一个参数为依赖模块列表,依赖模块加载后作为参数分别传入到回调函数 * define函数第二个参数为模块定义函数,将返回模块对象 */define(["jquery", "backbone"], function($, Backbone) {        //返回一个util模块对象.        return {            extend: function(dest, src) {                return $.extend(dest, src);            },            createModel : function() {            return Backbone.Model.extend({});            }        };    });
        上面介绍了如何在html页面引入require.js,如何定义一个module和如何使用module,这些都非常容易上手,当然,要想了解require.js的更多用法,还请参考官方网站:http://requirejs.org

        至此你已经可以构建一个结构良好的前端应用程序了。前面已经对类继承、对象扩展和模块化编程的具体实现原理做了简单分析,由于目前已有大量的js库已经实现了这些功能,所以我们只需直接拿来用即可。对于JavaScript编程热爱者,不仅要学会如何使用主流的JavaScript框架,还要理解其中设计原理,方能知其然知其所以然,在具体项目中遇到问题时,才不至于手足无措。

二. NodeJS入门

2.1 开发环境搭建

        Node.js是一个基于Chrome JavaScript引擎的运行平台,该平台用来构建高性能、伸缩性强的服务器端应用程序,Node.js是跨平台的轻量级引擎,它采用事件驱动和非阻塞I/O模型使得其适合编写高性能的、数据密集型的和实时性强的应用程序。

       安装node.js:windows平台直接访问官网http://nodejs.org/直接下载安装,linux和mac平台下可以通过git下载源码编译安装,见下面安装步骤:

git clone git://github.com/joyent//node.git
cd node
./configure
make
sudo make install

        安装通用插件。Node.js安装后便可以通过包管理器npm安装其他插件,其官网上https://npmjs.org/有大量的插件供开发者选择,下面将介绍几款比较常用的插件的安装。

  • mocha 单元测试框架

> npm install -g mocha

  • node-inspector 代码调试器

> npm install -g node-inspector

  • supervisor 热部署插件

> npm install -g supervisor

  • express 网站构建插件,详细请访问:http://expressjs.jser.us/

> npm install -g express

  • 前端应用构建工具Yeoman,详细请访问:http://yeoman.io/

> npm install -g yo

  • Karma测试驱动器(Test Runner),详情请访问:https://github.com/vojtajina/karma/

> npm install -g karma

        Nodejs的插件非常丰富,如数据库支持、集群、MVC框架、模板工具、图片处理、邮件服务等等,这些插件都可以在互联网获取。

2.2 hello world开始

        打开命令行窗口,输入node命令,回车进入运行窗口,输入JavaScript代码回车执行,返回结果,如下所示:
>node
>console.log(‘hello, world’);
hello, world

>

        两次按下ctrl+c即可退出node窗口运行模式。当然,node不限于此,我们可以把JavaScript逻辑写在js文件中,然后通过node命令来运行。

        编写hello.js:

function Hello() {   this.sayHello = function() {      console.log('hello, world');   };}var hello = new Hello();hello.sayHello();
        运行hello.js

>node hello.js

       运行结果:
       hello, world

2.3 NodeJS应用模块化

2.4.1 引用外部模块

        node既然作为一种服务脚本语言,跟其他服务器静态语言一样,需要一套完善函数库(类库),而组织类库或者编写第三方api均需要一个标准规范,node使用CommonJS规范来具体实施类库编写。CommonJS规范包括模块(module)、包(package)、单元测试等等内容。

        下面演示如何引用一个模块:

        编写foo.js

var circle = require('./circle.js');console.log( 'The area of a circle of radius 4 is '+ circle.area(4));
        上面代码通过require引入circle模块,并在后面直接调用circle的area方法计算半径为4的圆形面积。

2.4.2 构建自定义模块

        接着上面的例子,我们来看看circle模块如何定义,编写circle.js文件:

var PI = Math.PI;exports.area = function (r) {  return PI * r * r;};exports.circumference = function (r) {  return 2 * PI * r;};

       代码中exports向外面公开一个模块,即为circle模块,该模块申明了两个方法分别是area和circumference,一旦模块定义好,便可以在其他模块中通过require方法引用它。模块名称即为js文件名。

2.4 NodeJS常用API

2.5.1 File System

文件系统,即针对系统文件操作所提供的一套函数库,下面简单演示一下文件读取api:

var fs = require('fs');var fileName = 'D:/git/nodejs_workspace/oojs/test.js';fs.readFile(fileName, 'utf-8', function(err, fc) {    console.log(fc);});
以上程序将读取文件D:/git/nodejs_workspace/oojs/test.js的内容,并将其打印到控制台,注意,readFile方法是异步方法,由于文件读取是需要消耗磁盘I/O时间的,所以上面打印操作放在回调函数中执行,不会造成主体函数的阻塞等待问题,这个特性是其他静态语言无法相比的。

2.5.2 Http

一个简单的服务器应用,对所有HTTP请求均返回“hello, world”,编写server.js:

var http = require('http');http.createServer(function (req, res) {  res.writeHead(200, {'Content-Type': 'text/plain'});  res.end('Hello World\n');}).listen(1337, '127.0.0.1');console.log('Server running at http://127.0.0.1:1337/');
运行该应用:>node server.js

打开浏览器并输入http://127.0.0.1:1337,可以看到返回hello, world字符串。更多关于node服务器端编程接口,请访问:http://nodejs.org/api/http.html

三. JavaScript书籍推荐


《JavaScript权威指南》弗兰纳根(David Flanagan)


《JavaScript语言精粹(修订版)》道格拉斯•克罗克福德


《JavaScript高级程序设计(第3版)》尼古拉斯•泽卡斯


作者:肖波
日期:2014-06-13
QQ:691546285   欢迎交流~~
0 0
原创粉丝点击