JavaScript OOP

    1. JavaScript 数据类型

    JavaScript 是一种弱类型的动态语言:

    1. var foo = 42; // foo is a Number
    2. foo = "bar"; // foo is a String
    3. foo = true; // foo is a Boolean

    JavaScript 的基本类型有 boolean, null, undefined, number, string,基本类型是不可变的,除此之外都是对象类型。其中 boolean, number, string 有对应的包装对象,可以通过内置的构造函数 Number()String()Boolean() 来生成包装对象。

    包装对象带有一些常用的属性和方法,比如数字对象就带有 toFixed()toExponential() 等方法,字符串对象带有 substring()charAt()toLowerCase() 等方法及 length 属性。通过 typeof 运算符可以查询变量类型。

    JavaScript 对象

    JavaScript 对象可以看作属性的集合,每个属性是一组键值映射,其中键是字符串,值可以为任意类型(等价很多语言中的关联数组、哈希表、字典类型)。通过 {} 可以声明对象直接量,另外两种创建对象的方法是使用构造函数及 ES5 定义的 Object.create 方法。

    从 ES5 开始,有三种原生方法用于列举对象的属性:

    • for...in 循环:依次访问对象及其原型链中所有可枚举的属性
    • Object.keys(o):返回对象 o 自身包含(不包括原型中)的所有属性的名称的数组
    • Object.getOwnPropertyNames(o):返回一个数组,包含对象 o 所有拥有的属性(不论是否可枚举)的名称

    Chrome/Firefox 控制台中使用 console.dir 打印对象全部属性的键值。

    通过 delete 操作符可以删除变量及属性,设置属性为 undefined 或 null 并不能真正删除属性、而仅仅移除了属性和值的关联:

    1. var obj = {bar: 1, foo: 2, baz: 3};
    2. obj.bar = undefined;
    3. obj.foo = null;
    4. delete obj.baz;
    5. for (var i in obj) {
    6. if (obj.hasOwnProperty(i)) {
    7. console.log(i, '' + obj[i]);
    8. }
    9. }
    10. // bar undefined
    11. // foo null

    JavaScript 中对象是引用类型,因此将两个属性相同的对象对比会返回 false. 可以通过一些第三方库提供的 API 实现对象深度对比,Node 中的 assert 模块提供了 assert.deepEqual(actual, expected, [message]) 方法。

    1. var a = {name: 'a'};
    2. var b = {name: 'a'};
    3. a == b // false
    4. a === b // false
    5. var c = a;
    6. c === a // true

    JavaScript 函数

    JavaScript 函数是一个附带可被调用功能的常规对象。

    // TODO


    2. JavaScript prototype

    // TODO

    3. 基于原型的类定义

    类是对象的模板,定义了同一组对象共用的属性 ( property ) 和方法 ( method ), JavaScript 中没有类的原生定义,实例对象是从原型对象 ( prototypical object ) 调用构造函数 new 生成(ES5 提供了 Object.create 方法,同样用于创建一个拥有指定原型和若干指定属性的对象)

    1. // Constructor
    2. var Animal = function(name) {
    3. // property
    4. this.name = name;
    5. };
    6. Animal.prototype.home = 'Earth';
    7. Animal.prototype.breath = function() {
    8. console.log('breath');
    9. };

    创建一个实例对象

    1. var cat = new Animal('susan');
    2. console.log(cat.name); // 'susan'
    3. console.log(cat.home); // 'Earth'
    4. cat.breath(); // 'breath'
    5. // ES5 Syntax
    6. Object.create(proto, [ propertiesObject ])

    两种对象创建方法的使用选择,参考:http://stackoverflow.com/questions/2709612/using-object-create-instead-of-new

    判断一个原型对象和实例之间的关系

    1. Animal.prototype.isPrototypeOf(cat)
    2. cat.constructor === Animal

    判断一个属性是本地属性还是继承自原型对象

    1. cat.hasOwnProperty('name') // true
    2. cat.hasOwnProperty('home') // false

    in 运算符判断一个对象是否具有某属性(不区分本地属性 / 原型对象属性)

    1. 'name' in cat // true
    2. 'home' in cat // true

    在基于类的对象系统中,往往无法在运行时动态添加属性,而 JavaScript 允许动态地向单个对象或整个对象集中添加或移除属性。

    1. var data = {
    2. 'prop1': 1, 'prop2': 2
    3. };
    4. data['prop3'] = 3;
    5. data.prop4 = 4;
    6. Object.defineProperty(data, 'prop5', {
    7. value: 6,
    8. writeable: true,
    9. enumerable : true,
    10. configurable : true
    11. });

    3. 基于原型的类继承

    JavaScript 中任何对象可以作为另一个对象的原型对象,以此来共享属性。当读取一个对象的属性时,首先在本地对象中查找,进而沿着原型继承链向上查找,直到 Object.prototype

    给子类定义一个构造函数,将 prototype 对象指向父类的实例:

    1. var Dog = function() {};
    2. Dog.prototype = new Animal();
    3. Dog.prototype.wag = function() {
    4. console.log('wag tail');
    5. };

    通过 Object.create 实现类继承:

    1. // Shape - superclass
    2. function Shape() {
    3. this.x = 0;
    4. this.y = 0;
    5. }
    6. Shape.prototype.move = function(x, y) {
    7. this.x += x;
    8. this.y += y;
    9. console.info("Shape moved");
    10. }
    11. // Rectangle - subclass
    12. function Rectangle() {
    13. Shape.call(this); // call super constructor
    14. }
    15. Rectangle.prototype = Object.create(Shape.prototype); // inherit
    16. // Multiple inheritance
    17. function RectA() {
    18. Shape.call(this);
    19. Rectangle.call(this);
    20. }
    21. RectA.prototype = Object.create(Shape.prototype); // inherit
    22. mixin(Shape.prototype, Rectangle.prototype); // mixin

    mixin 的行为是将超类原型上的函数拷贝到子类原型上,类似 jQuery.extend

    5. 函数调用(apply, call, bind)

    使用 apply()call() 允许在一个对象上调用另一个对象上的方法。两者区别在于传入函数的参数形式 ( 助记:“A for array and C for comma.” )

    Syntax:

    1. fun.apply(thisArg, [argsArray])
    2. fun.call(thisArg[, arg1[, arg2[, ...]]])

    使用 call() 调用父构造函数

    1. function Product(name, price) {
    2. this.name = name;
    3. this.price = price;
    4. }
    5. function Food(name, price) {
    6. Product.call(this, name, price);
    7. this.category = 'food';
    8. }

    使用 apply() 寻找一个数组中最大 / 最小值

    1. var numbers = [5, 6, 2, 3, 7];
    2. var max = Math.max.apply(null, numbers);
    3. var min = Math.min.apply(null, numbers);

    另一个常用场景是,将一个函数调用委托 ( proxy ) 给另一个调用,以及修改传入参数。常见于一些框架实现:

    1. var logger = {
    2. log: function () {
    3. var args = Array.prototype.slice.call(arguments);
    4. args.unshift('[' + new Date().toLocaleString() + ']');
    5. console.log.apply(console, args);
    6. }
    7. };

    这里的 arguments 变量是当前函数作用域内的参数数组,注意 arguments 对象不是一个真正的 Array, 没有数组特有的属性和方法(除了 length),例如它没有 pop() 方法;通过 Array.prototype.slice.call(arguments) 将其转换为数组。

    ES5 中加入了 bind() 函数以控制调用的作用域,bind() 会创建一个绑定函数,与被调函数有相同的函数体。确保函数是在指定 this 所在的上下文中调用

    1. var user = {
    2. data: [
    3. { name: "Alex", age: 27 },
    4. { name: "Bob", age: 23 }
    5. ],
    6. clickHandler: function(event) {
    7. var idx = Math.floor(Math.random() * 2); // random between 0, 1
    8. $("input").val(this.data[idx].name + " " + this.data[idx].age);
    9. }
    10. }
    11. $("button").click(user.clickHandler); // Error
    12. $("button").click(user.clickHandler.bind(user));

    bind 配合 setTimeout: 使用 window.setTimeout() 时,this 关键字会指向 window(或全局)对象。当使用类的方法时,需要 this 引用类的实例,你可能需要显式地把 this 绑定到回调函数以便继续使用实例

    1. function LateBloomer() {
    2. this.petalCount = Math.ceil(Math.random() * 12) + 1;
    3. }
    4. // Declare bloom after a delay of 1 second
    5. LateBloomer.prototype.bloom = function() {
    6. window.setTimeout(this.declare.bind(this), 1000);
    7. };
    8. LateBloomer.prototype.declare = function() {
    9. console.log('I am a beautiful flower with ' +
    10. this.petalCount + ' petals!');
    11. };
    12. var flower = new LateBloomer();
    13. flower.bloom();

    6. 定义类的私有函数

    将类的属性包装到一个匿名函数,并创建局部变量,并定义为类的属性

    1. var Person = function() {};
    2. (function() {
    3. var findByName = function(name) {
    4. console.log('Anonymous ' + name);
    5. }
    6. Person.find = function(name) {
    7. if (typeof name === 'string') {
    8. return findByName(name);
    9. }
    10. }
    11. })();

    JavaScript 变量作用域

    JavaScript 中变量作用域是函数级的,没有块级作用域。例如:

    1. function foo() {
    2. for (var i = 0; i < 10; i++) {
    3. var value = "hello world";
    4. }
    5. console.log(i); // 10
    6. console.log(value); // hello world
    7. }

    因此要避免就近变量声明,更多放在函数体顶部。


    事件和监听

    1. 事件类型 / 事件对象

    如果一个节点和它的父节点都绑定相同类型的回调,事件触发时按照执行顺序可以分类:

    • 事件捕捉(capturing):从顶级父节点开始触发事件,从外向内传播
    • 事件冒泡(bubbling):从最内层逐级冒泡直至顶层节点,从内向外传播

    绑定事件监听器:

    1. // 第三个参数 useCapture 指定事件类型,默认 false 即冒泡
    2. button.addEventListener("click", function() { /* ... */ }, false);
    3. button.addEventListener("click", function(e) {
    4. // 终止事件冒泡,父节点的事件回调都不会触发
    5. e.stopPropagation();
    6. });

    阻止事件默认行为,通过调用 event 对象的 preventDefault() 函数,或在回调中返回 false.

    2. 委托事件(Delegation)

    给父节点绑定事件监听,用于检测子元素内的事件;可以用来减少应用中事件监听器的数量

    1. list.addEventListener("click", function(e) {
    2. if (e.currentTarget.tagName === "li") {
    3. /* ... */
    4. return false;
    5. }
    6. }, false);

    jQuery 的 delegate() 方法传入子元素选择器、事件类型和回调,即可实现父节点绑定:

    1. $("ul").delegate("li", "click", /* ... */);

    使用事件委托的另一个好处是,动态添加的子元素也都具有事件监听;上例中页面载入完成后添加的 li 节点同样可以触发事件的回调。

    3. 自定义事件

    jQuery 提供了 trigger() 函数来触发自定义事件,可以通过命名空间的形式来管理事件名称:

    1. // 绑定自定义事件
    2. $(".class").bind("refresh.widget", function() {});
    3. // 触发自定义事件
    4. $(".class").trigger("refresh.widget");

    通过传入一个额外参数来传递数据,数据会以附加参数的形式带入回调:

    1. $(".class").bind("frob.widget", function(e, dataNumber) {
    2. console.log(dataNumber):
    3. });
    4. $(".class").trigger("frob.widget", 5);

    和内置事件一样,自定义事件同样会沿着 DOM 树做冒泡。

    4. DOM 无关事件

    基于事件的编程非常强大,因为它能让应用架构充分解耦,让功能变得更加内聚且具有更好可维护性。事件本质是 DOM 无关的,因此可以很容易开发出一个事件驱动的库。这种模式称为发布/订阅(Pub/Sub):发布者向某个信道(channel)发布一条消息,订阅者绑定这个信道,当有消息发布时会接受到一个通知。发布者和订阅者是完全解耦的,两者仅共享一个信道名称。

    1. var PubSub = {
    2. subscribe: function(ev, callback) {
    3. var calls = this._callbacks || (this._callbacks = {});
    4. (this._callbacks[ev] || (this._callbacks[ev] = [])).push(callback);
    5. return this;
    6. },
    7. publish: function() {
    8. var args = Array.prototype.slice.call(arguments, 0);
    9. var ev = args.shift();
    10. var list, calls;
    11. if (!this._callbacks || !this._callbacks[ev]) {
    12. return this;
    13. } else {
    14. calls = this._callbacks;
    15. list = this._callbacks[ev];
    16. }
    17. for (var i = 0, l = list.length; i < l; i++) {
    18. list[i].apply(this, args);
    19. }
    20. return this;
    21. }
    22. };
    23. PubSub.subscribe('Test', function() {
    24. console.log('Test');
    25. });
    26. PubSub.publish('Test');

    模型和数据

    1. 命名空间

    MVC 模式中,数据管理由模型负责,与数据操作和行为相关的逻辑都应放入模型中,通过命名空间管理。

    在 JavaScript 中,通过给对象添加属性和方法以实现命名空间:

    1. var User = {
    2. records: [ /* ... */ ],
    3. fetchRemote: function() { /* ... */ }
    4. };

    也可以通过定义一个类,实例化相关的模型对象:

    1. var User = function(atts) {
    2. this.attributes = atts || {};
    3. };
    4. User.prototype.destroy = function() {
    5. /* ... */
    6. };
    7. User.fetchRemote = function() {
    8. /* ... */
    9. };

    2. ORM

    对象关系映射(ORM)是一种常用的技术,可以将模型和远程服务 / 页面 DOM 元素等绑定在一起。

    本质上,ORM 是一个包装了数据的对象层;以往 ORM 常用于抽象数据库,这里 ORM 只用于抽象 JavaScript 数据类型。额外的层带来的好处是:可以通过给它添加自定义的函数和属性、来增强基础数据的功能,如添加数据合法性校验、持久化、服务器端的回调处理等,以增加代码的重用率。

    大部分 JavaScript ORM 是和前端 MVC 框架结合使用的。

    3. 数据加载

    数据可以直接嵌入初始页面(后端模版渲染),但不利于缓存。也可以通过 Ajax 或 JSONP 方式动态加载数据。


    控制器和状态

    状态可以使用 session cookie 保存,但对于单页应用(SPA)而言,状态保存在客户端。当使用了 MVC 框架之后,状态使用控制器(Controller)进行管理。

    1. 状态机

    有限状态机(Finite State Machines, FSM)是编写 UI 程序的利器,使用状态机可以轻松管理很多控制器,按需显示和隐藏视图。

    状态机本质上有两部分组成:状态和转换器,只有一个活动状态(active state),有很多非活动状态(passive state),当状态切换时会调用状态转换器。

    首先,使用 jQuery 的事件 API 创建一个 Events 对象,给它添加绑定和触发状态机的事件的能力:

    1. var Events = {
    2. bind: function() {
    3. if (!this.o) {
    4. this.o = $({});
    5. }
    6. this.o.bind.apply(this.o, arguments);
    7. },
    8. trigger: function() {
    9. if (!this.o) {
    10. this.o = $({});
    11. }
    12. this.o.trigger.apply(this.o, arguments);
    13. },
    14. };

    创建 StateMachine 类

    1. var StateMachine = function() {};
    2. StateMachine.fn = StateMachine.prototype;
    3. // 添加 bind 和 trigger
    4. $.extend(StateMachine.fn, Events);
    5. StateMachine.fn.add = function(controller) {
    6. this.bind("change", function(e, current) {
    7. if (controller == current) {
    8. controller.activate();
    9. } else {
    10. controller.deactivate();
    11. }
    12. });
    13. controller.active = $.proxy(function() {
    14. this.trigger("change", controller);
    15. }, this);
    16. };

    这个状态机的 add() 函数将传入的 controller 加入到状态列表、并创建一个 active() 函数。当调用 active() 时,控制器的状态就会转换为 active 状态;对于 active 状态的控制器,状态机将基于它调用 activate()、对于其他控制器调用 deactivate()

    1. var controller1 = {
    2. activate: function() {
    3. $("#con1").addClass("active");
    4. },
    5. deactivate: function() {
    6. $("#con1").removeClass("active");
    7. },
    8. };
    9. var controller2 = {
    10. activate: function() {
    11. $("#con2").addClass("active");
    12. },
    13. deactivate: function() {
    14. $("#con1").removeClass("active");
    15. },
    16. };
    17. var sm = new StateMachine;
    18. sm.add(controller1);
    19. sm.add(controller2);
    20. controller1.active();

    2. 路由控制

    通过 URL Hash 或 HTML5 History API.


    视图和模版

    将应用的重心从服务器端迁往客户端的过程:

    • 将视图所需的数据迁往客户端,通过 Ajax 请求、并由应用的模型载入数据
    • 不必在服务端对视图进行预渲染、而是全部放在客户端
    • 不使用 JavaScript 动态创建 DOM 的形式(如果需渲染的视图元素不多、则建议使用)、而是使用模版生成视图

    JavaScript 模版的核心概念是,将包含模版变量的 HTML 片段和 JavaScript 对象做合并,将模版变量替换为对象中属性。模版的功能不仅限于单纯的插值替换,还有一些条件流(conditional flow)和迭代。

    1. 绑定

    绑定会将视图元素和 JavaScript 对象(通常是模型)关联在一起,当 JavaScript 对象变化时、视图会根据新修改后的对象做适时更新。

    即一旦将视图和模型绑定,当应用的模型更新时、视图也会自动渲染,而控制器不必处理视图的更新(MVVM)


    依赖管理

    前端模块系统的工程意义:复用、分治,解决模块的定义 / 依赖 / 导出,前端开发特征:

    • 基于多语言、多层次的编码和组织工作
    • 前端产品的交付是基于浏览器、且通过增量加载的方式执行在客户端浏览器

    对于模块的加载和传输,两种极端方式:

    • 每个模块单独请求(会导致交互行为迟滞)
    • 所有模块打包后请求一次(会导致流量浪费、首次加载慢)

    普遍使用按需懒加载,即分块传输。CSS 模块实现,由 less / sass / stylus 等预处理器的 import / mixin 特性支持。对于 JavaScript 模块系统的演进:

    1. <script> 标签

    最原始的 JavaScript 文件加载方式,把每个文件看成一个模块,其接口通常暴露在全局作用域下(window 对象中)。弊端有:

    • 文件只能按 <script> 顺序加载
    • 容易变量冲突;可以采用命名空间的概念组织各模块的接口,以规避该问题

    2. CommonJS

    模块通过 require 方法同步加载依赖的其他模块,通过 exports 导出要暴露的接口。弊端有:

    • 不适合浏览器环境(异步加载),同步意味着阻塞加载

    3. AMD

    AMD 规范只有一个主要接口

    1. define(id?: String, dependencies?: String[], factory: Function|Object)

    它在声明模块时指定所有的依赖,并且当作形参传到 factory. 依赖前置,即对于依赖的模块提前执行。

    1. define("module", ["dep1", "dep2"], function(d1, d2) {
    2. return someExportedValue;
    3. });
    4. require(["module", "../file"], function(mod, fs) {
    5. /* ... */
    6. });

    适合在浏览器中异步加载模块,可以并行加载多个模块,弊端在于开发成本高。

    4. ES6 Module

    ES6 标准增加了 JavaScript 语言层面的模块体系定义,其设计思想是尽量静态化,使得编译时就能确定模块的依赖关系、以及输入和输出的变量。