下半部分,主要涉及浏览器端,还有最佳实践和高级用法,尤其是高级技巧这一章,讲解了很多实战中会用到的有用技巧,让我来拓展一下吧。

JavaScript高级程序设计

作者 Nicholas C. Zakas

事件

  • 事件冒泡:最后冒泡到 window 对象
  • DOM 事件流的三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段
  • HTML 事件处理程序在运行时有权利访问所有全局作用域的所有代码(一般 HTML 事件被封装在 try-catch 块中以避免页面解析前触发事件程序)

DOM2级事件处理程序

  • addEventListener()removeEventListener(),第三个参数布尔值指定事件是在冒泡阶段触发(false)还是捕获阶段触发(true)
  • 使用 attachEvent() 方法的情况下,事件处理程序会在全局作用域中运行,因此 this 指向 window
  • event 对象中,thisevent.currentTarget 相同,均指向事件注册的元素,而 event.target 则指向实际事件的触发元素,在 IE8 中,前者指向 window
  • preventDefault() 阻止默认事件,在 IE 中则是 window.event.returnValue = false
  • stopPropagation() 阻止事件冒泡,在 IE 中则是 window.event.cancelBubble = true
  • 跨浏览器的事件处理程序
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    var EventUtil = {
    addHandler: function (element, type, handler) {
    if (element.addEventListener) {
    element.addEventListener(type, handler, false)
    } else if (element.attachEvent) {
    element.attachEvent('on' + type, handler)
    } else {
    element['on' + type] = handler
    }
    },
    getEvent: function (event) {
    return event ? event : window.event
    },
    getTarget: function (event) {
    return event.target || event.srcElement
    },
    preventDefault: function (event) {
    if (event.preventDefault) {
    event.preventDefault()
    } else {
    event.returnValue = false
    }
    },
    stopPropagation: function (event) {
    if (event.stopPropagation) {
    event.stopPropagation()
    } else {
    event.cancelBubble = true
    }
    },
    removeHandler: function (element, type, handler) {
    if (element.removeEventListener) {
    element.removeEventListener(type, handler, false)
    } else if (element.detachEvent) {
    element.detachEvent('on' + type, handler)
    } else {
    element['on' + type] = null
    }
    }
    }
    //使用
    EventUtil.addHandler(btn, 'click', handler)
    EventUtil.removeHandler(btn, 'click', handler)

鼠标与键盘事件

  • 特殊事件
    • blurfocus 事件不会冒泡,因此可用 focusinfocusout 模拟替代
    • click 由鼠标单击或按下回车键触发,而 mousedownmouseup 则不支持键盘触发
    • 鼠标事件中,mouseentermouseout 不会冒泡
    • mousewheel 事件会冒泡到 window ,通过检测 event.wheelDelta 的正负号可判断滚动方向
    • 触摸设备不支持 dbclick,此外还会有点击的 300ms 延迟
  • 坐标
    • clientXclientY 保存鼠标相对 ViewPort 的位置,pageXpageY (IE8不支持)则相对 document 页面位置(跨浏览器方案如下),screenXscreenY 则保存着相对于屏幕的位置
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      var div = document.getElementById('myDiv')
      EventUtil.addHandler(div, 'click', function (event) {
      event = EventUtil.getEvent(event)
      var pageX = event.pageX,
      pageY = event.pageY
      if (pageX === undefined) {
      pageX = event.clientX + (document.body.scrollLeft || document.documentElement.scrollLeft) //IE8
      }
      if (pageY === undefined) {
      pageY = event.clientY + (document.body.scrollTop || document.documentElement.scrollTop) //IE8
      }
      alert('Page coordinates: ' + pageX + ',' + pageY)
      })

其他

  • IE9+ 支持变动事件,可以判断 DOM 结构的变动,如删除节点或插入节点
  • 使用 contextmenu 事件可以模拟出自定义上下文菜单,在 Web 应用中达到原生的体验
  • window.onload 不同(所有资源下载完毕),DOMContentLoaded 事件(IE9+)在形成完整 DOM 树时触发(与jQuery.ready()类似)
  • IE8+ 支持 window.hashchange 事件,在 URL 的参数列表(及 URL 中 # 号后面的所有字符串)发生变化时触发,该方法可以实现一些简单的路由以监听 URL 变化
  • 移动设备中有 window.orientation 属性,设备方向改变时触发 orientationchange 事件,而 deviceorientation事件可根据陀螺仪感应判断

内存和性能

  • 事件委托

    利用事件委托的冒泡,可以将事件统一绑定在 document (或其他方便统一管理的 DOM)上,这是对事件最常用对优化

  • 移除不需要的时间处理程序,常常用于删除 DOM 后释放内存

表单脚本

  • submit 事件可能在点击提交按钮后触发,也可能在之前触发
  • 在不支持 readonly 特性的浏览器中,可以用 blur 方法来创建只读表单

    1
    2
    3
    <!-- 显示25个字符,最多输入50个字符,带初始值 -->
    <input type="text" size="25" maxlength="50" value="initial value">
    <textarea rows="25" cols="5">initial value</textarea>
  • HTML5 中新增的一些表单 API 可以考虑在移动开发时适当应用

JSON

  • JSON 是一种数据格式,并不从属于 JavaScript
  • 支持三种类型的值:简单值(字符串 / 数组 / 布尔值 / null)、对象数组,不支持 undefined
  • 字符串与对象名严格要求使用双引号
  • JSON的早期解析使用 eval() 函数,IE8+ 支持vJSON 对象,该对象有两个方法 JSON.stringify()JSON.parse(),分别用于把
    JavaScript 对象序列化为 JSON 字符串和把 JSON 字符串解析为原生 JavaScript 值。
  • JSON.stringify() 可以接受两个可选参数,第一个参数(数组或函数)用来过滤输出的值,第二个参数表示输出值是否保留缩进

AJAX与Comet

这一章其实涉及到了很多前端开发会用到的通讯技术,因此最合适的方法是深入做一个总结并针对其优缺点进行考量

1
2
3
4
5
6
7
8
9
10
11
12
13
var xhr = createXHR(); //兼容IE7+
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText)
} else {
alert("Request was unsuccessful: " + xhr.status)
}
}
};
xhr.open('get', 'example.txt', true) //启动XHR
xhr.setRequestHeader('MyHeader', 'MyValue') //设置头部信息
xhr.send(null) //发送XHR
  • FormData 类型(IE10+)
  • AJAX 跨域
    • CORS
    • 图像 Ping
    • JSONP

高级技巧

高级函数

  • 安全的类型检查 => Object.prototype.toString.call()
  • 作用域安全的构造函数——避免使用构造函数时前面忘记加 new

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function Person (name, age, job) {
    if (this instanceof Person) {
    this.name = name
    this.age = age
    this.job = job
    } else {
    return new Person(name, age, job)
    }
    }
  • 惰性载入函数(初始化性能还是初次运行性能?)

    1. 第一次调用函数时判断浏览器兼容后,重新赋值对应浏览器兼容的方法给该函数,后续再次调用该函数时就直接使用最新兼容方法而无需再次判断兼容性
    2. 声明函数时直接判断好浏览器的兼容性返回对应能力的处理函数,这种方法只在首次加载时进行能力判断
  • 函数绑定

    1
    2
    3
    4
    5
    6
    function bind (fn, context) {
    return function () { // 返回一个函数 binded(...arguments),运行这个函数就会执行作用域绑定后的原函数
    return fn.apply(context, arguments)
    }
    }
    // 在 ES5 环境中原生支持这个方法 .bind()
  • 函数柯里化

    柯里化的实现思路是把原函数的参数分离

1
2
3
4
5
6
7
8
function curry (fn, ...args) { // 这是原始函数,第一个参数是要柯里化的函数,其他是参数
var outterArgs = Array.prototype.slice.call(arguments, 1) // 因为原始函数第一个参数是 fn,因此提取出后面的参数
return function () { // 生成新函数并返回
var innerArgs = Array.prototype.slice.call(arguments) // 这是传入新函数的参数
var finalArgs = outterArgs.concat(innerArgs) // 合并原始函数和生成新函数的参数
return fn.apply(null, finalArgs)
}
}

防篡改对象

  • 不可扩展对象(Object.preventExtensions()
  • 密封的对象(Sealed Object - ES5+)
  • 冻结的对象(Frozen Object)

高级定时器

这里入门体现了 JavaScript 的事件队列

  • 重复的定时器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 由于事件队列的实现方式,setTimeout 并不精确,考虑以下场景
    setInterval(function () {
    // 这里放入复杂的运算,需要超过 100ms 才能执行完毕
    // 导致的问题是,有些运算可能会被跳过,执行的间隔跟预期差太多,尤其是前者的问题会更严重一些
    }, 100)
    // 使用 setTimeout 模拟,这样可以保证运算不会被跳过,并且执行的间隔一致性会更好
    // 实际开发中可能很少需要考虑这种,因为一般前端的运算时间误差都是可以接受的,但是总有一天会用得到
    var task = function () {
    console.log('doing')
    // 执行复杂的运算
    setTimeout(task, 100)
    }
    task() // 首次执行
  • Yielding Processes

    这里充分利用了 JavaScript 事件队列的特性,解决了单线程容易卡死的问题
    建议是一个任务如果需要超过 50ms 的时间来执行,就可以考虑

1
2
3
4
5
6
7
8
9
// 处理一个非常大的数据块,可能需要几十秒的时间,为了防止界面卡死,可以使用分割处理的方法
function chunk (data) {
var piece = data.splice(0, 10000) // 抽取出前 10000 个数据
// 处理 piece
if (data.length) {
setTimeout(chunk, 0, data) // 如果还有未处理的的数据,则等到下一个事件调度执行
}
}
chunk(veryBigData) // 传入一个巨大的数组

函数节流与防抖

  • 函数节流(Throttle)

    一般这个用在防止多次触发高消耗操作的地方,比如滚动时动态加载数据等

1
2
3
4
5
6
function throttle (method, context) {
clearTimeout(method.id) // 如果触发了 throttle,则先清除上一次的事件,然后重新绑定新倒计时
method.id = setTimeout(function () { // 函数也可以定义属性,这样就节省了一个外部变量来保存 id
method.call(context)
}, 100) // 这里使用定时器是监听触发间隔不会太频繁
}
  • 函数防抖(Debounce)

    这里引申一个函数防抖,这是基于函数节流的原理的。
    前面的函数节流如果一直触发事件,那么可能一直也不会进入处理方法
    防抖的意思是在指定间隔内跟节流一样不会触发多次,但至少会触发一次

自定义事件

这其实就是大名鼎鼎但观察者模式,实现里一个事件监听队列,是松散代码耦合但一种有效模式

  • 概念

    由两种对象组成:观察者和主体
    主体负责发布事件,观察者可以通过订阅的方式监听主体
    关键点在于:主体并不知道观察者的存在,而观察者则知道主体的运作方式等(类似原生的 DOM 事件,DOM 是主体,而事件处理程序是观察者,一个主体可以有多个观察者)

离线应用与客户端存储

  • 离线检测
    • navigator.onLine 检测当前是否联网
    • 在线与离线时触发事件 onlineoffline
  • 应用缓存(application cache)

    • manifest 属性指定缓存文件(<html manifest="/offline.manifest">),其 MIME 类型必须是 text/cache-manifest 推荐扩展名为 .appcache
    • 手动更新 applicationCache.update()
  • HTTP Cookie

    • 名称,不区分大小写
    • 值,URL 编码后的字符串
    • 域,生效域,默认为来源
    • 路径,相对域的路径
    • 失效时间,GMT 格式
    • 安全标志,只在 SSL 链接发送
  • JavaScript 通过 document.cookie 获取,使用 decodeURIComponent() 解码

  • 封装的 Cookie 方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    var CookieUtil = {
    get: function (name) {
    var cookieName = encodeURIComponent(name) + '=',
    cookieStart = document.cookie.indexOf(cookieName),
    cookieValue = null
    if (cookieStart > -1) {
    var cookieEnd = document.cookie.indexOf(';', cookieStart)
    if (cookieEnd == -1) {
    cookieEnd = document.cookie.length
    }
    cookieValue = decodeURIComponent(document.cookie.substring(cookieStart + cookieName.length, cookieEnd))
    }
    return cookieValue
    },
    set: function(name, value, expires, path, domain, secure) { //前两个参数必须
    var cookieText = encodeURIComponent(name) + "=" + encodeURIComponent(value);
    if (expires instanceof Date) {
    cookieText += '; expires=' + expires.toGMTString()
    }
    if (path) {
    cookieText += '; path=' + path
    }
    if (domain) {
    cookieText += '; domain=' + domain
    }
    if (secure) {
    cookieText += '; secure'
    }
    document.cookie = cookieText
    },
    unset: function(name, path, domain, secure) {
    this.set(name, '', new Date(0), path, domain, secure)
    }
    };

最佳实践

  • 可维护的代码

    • 可理解性 + 直观性 + 可适应性 + 可扩展性 + 可调试性
    • 松散耦合
      • 勿将 Event 对象传给其他方法;只传来自 event 对象中所需的数据
      • 任何可以在应用层面的动作都应该可以在不执行任何事件处理程序的情况下进行
      • 任何事件处理程序都应该处理事件,然后将处理转交给应用逻辑
  • 性能优化

    • 避免全局查找 —— 使用局部变量引用上层变量(尤其是DOM)
    • 避免不必要的属性查找 —— 查找常量和数组比查找属性更高效
    • 循环优化
      • 减值迭代更加高效
      • 简化终止条件
      • 简化循环体的计算量
      • do-while 可以避免终止条件的计算
      • 当循环次数确定时,不用循环而使用多次调用性能更好
    • 最小化语句数(UglifyJS 已经帮你考虑一些性能更好的优化)

新兴的API

  • requestAnimationFrame(平滑的脚本动画)

    一般显示器的刷新率是 60Hz,因此最平化动画的循环间隔是 1000ms/60,约 17ms

  • Page Visibility API(页面是否激活)

  • Geolocation API(地理定位)

    使用 navigator.geolocation 对象(IE9+),和消息提示类似,需要用户手动授权

  • File API (本地文件 - IE10+)

  • Web 计时
  • Web Workers