美文网首页
高级技巧

高级技巧

作者: 了凡和纤风 | 来源:发表于2019-08-21 15:38 被阅读0次

    本章内容:使用高级函数、防篡改对象、Yielding with Timers

    JavaScript 是一种极其灵活的语言,由于它天生的动态属性,这种语言还能使用 更为复杂和有趣的模式。这些技巧要利用在 ECMAScript 的语言特定、BOM扩展 和 DOM 功能来获得强大的效果。

    一、高级函数

    函数是 JavaScript 中最有趣的部分之一。他们本质上是十分简单和过程化的,但也可以是非常复杂和动态的。

    1.1、安全的类型检测

    JavaScript 内置的类型检测机制并非完全可靠。

    • typeof——由于他有一些无法预知的行为,经常会导致检测数据类型时得不到靠谱的结果。
    • instanceof——操作符在存在多个全局作用域(像一个页面包含多个 frame)的情况下,也是问题多多

    解决上述问题的办法都一样。.在任何值上调用 Object 原生对象的 toString() 方法,都会返回一个 [object NativeConstructorName]格式的字符串。每个类在内部都有一个 [ [Class] ]属性,这个属性中就指定了上述字符串中的构造函数名。

    Object.prototype.toString.call([]) // "[object Array]"
    

    基于这一思路可以创建以下类似的工具函数:

    function isFunction(value) {
      return Object.prototype.toString.call(value) == "[object Function]"
    }
    
    function isArray(value) {
      return Object.prototype.toString.call(value) == "[object Array]"
    }
    

    注意:对于在IE中以 COM 对象形式实行的任何函数,isFunction() 都将返回 false

    1.2、作用域安全的构造函数

    先来回顾以下构造函数的创建及使用

    function Person(name, age, job) {
      this.name = name
      this.age = age
      this.job = job
    }
    
    var person = new Person('了凡子', 1, 'pet')
    

    问题出来当没有 使用 new 操作符来调用该构造函数的情况上。由于该 this 对象是在运行时绑定的,所以直接调用 Person(), this 会映射到全局对象 window 上,导致错误对象属性的意外增加。 如下:

    var person = Person('xx', 11, 'xx')
    console.log(window.name) // xx
    console.log(window.age) // 11
    console.log(window.job) // xx
    

    这个问题的解决方法是创建一个作用域安全的构造函数。作用域安全的构造函数在进行任何更改前,首先确认 this 对象是正确类型的实例。如果不是,那么会创建新的实例并返回。

    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)
      }
    }
    
    var p1 = Person('xxx', 16, 'xx')
    console.log(window.name) // ''
    console.log(p1.name) // xxx
    
    var p2 = new Person('xxx', 12, 'xxx')
    console.log(p2.name) // xxx
    

    实现这个模式后,你就锁定了 可以调用 构造函数的环境。如果你使用构造函数窃取模式的继承且不使用原型链,那么这个继承很可能被破坏。
    如下示例:

    function Polygon(sides) {
      if (this.instanceof Polygon) {
        this.sides  = sides
        this.getArea = function() {
          return 0
        }
      } else {
        return new Polygon(sides)
      }
    }
    
    function Rectangle(width, height) {
      Polygon.call(this, 2)
      this.width = width
      this.height = height
      this.getArea = function() {
        return this.width * this.height
      }
    }
    
    var rect = new Rectangle(5, 10)
    console.log(rect.sides) // undefined
    

    这个例子中,Rectangle 构造函数中的 this 对象并没有得到增长,同时 Polygon.call() 返回的值也没有用到,所以 Rectangle 实例中不会有 sides 属性。

    如果构造函数窃取结合使用原型链或寄生组合则可以解决这个问题:

    function Polygon(sides) {
      if (this.instanceof Polygon) {
        this.sides  = sides
        this.getArea = function() {
          return 0
        }
      } else {
        return new Polygon(sides)
      }
    }
    
    function Rectangle(width, height) {
      Polygon.call(this, 2)
      this.width = width
      this.height = height
      this.getArea = function() {
        return this.width * this.height
      }
    }
    
    Reactangle.prototype = new Polygon()
    
    var rect = new Rectangle(5, 10)
    console.log(rect.sides) // 2
    

    多个程序员在同一个页面上写 JavaScript 代码的环境中,作用域安全构造函数就很有用了。届时,对全局对象以外的更改可能会导致一些常常难以追踪的错误。除非你单纯基于构找函数窃取来实现继承,推荐作用域安全的构造函数作为最佳实践。

    1.3、惰性载入函数

    因为浏览器之间行为的差异,多数 JavaScript 代码包含了 大量的 if 语句,将执行引导到正确的代码中。
    比如 跨浏览器注册事件

    function addHandler(element, type, handler) {
        if (element.addEventListener) { // 支持 addEventListener
          element.addEventListener(type, handler, false)
        }else if (element.attachEvent) { // IE
          element.attachEvent('on' + type, handler)
        } else { // 都不支持的情况下,默认使用 DOM0 级
          element['on' + type] = handler
        }
      }
    

    即使只有一个 if 语句的代码,也肯定要比没有 if 语句的 慢,所以如果 if 语句不必每次执行,那么代码可以运行地更快一些。解决方案就是称之为惰性载入的技巧


    惰性载入表示函数执行的分支仅会执行一次。有两种实现惰性载入的方式:

    • 第一种就是在函数被调用时再处理函数。在第一调用的过程中,该函数会覆盖为另一个按合适方法执行的函数,这样任何对原函数的调用都不用再经过执行的分支了。
    function addHandler(element, type, handler) {
        if (element.addEventListener) { // 支持 addEventListener
          addHandler = function(element, type, handler){
            element.addEventListener(type, handler, false)
          }
        }else if (element.attachEvent) { // IE
          addHandler = function(element, type, handler){
            element.attachEvent('on' + type, handler)
          }
        } else { // 都不支持的情况下,默认使用 DOM0 级
          addHandler = function(element, type, handler){
            element['on' + type] = handler
          }
        }
      }
    

    if 语句的每一个分支都会为 addHandler 赋值,有效覆盖率原有的函数。

    • 第二种实现惰性载入的方式是在声明函数时就指定适当的函数。这样,第一次调用函数时就不会损失性能了,而在代码首次加载时会损失一点性能。
    var addHandler = (function() {
          if (element.addEventListener) { // 支持 addEventListener
          reurn function(element, type, handler){
            element.addEventListener(type, handler, false)
          }
        }else if (element.attachEvent) { // IE
          return function(element, type, handler){
            element.attachEvent('on' + type, handler)
          }
        } else { // 都不支持的情况下,默认使用 DOM0 级
          return function(element, type, handler){
            element['on' + type] = handler
          }
        }
    })();
    

    使用的技巧是创建一个匿名、自执行的函数,用以确定应该使用哪一个函数实现。

    惰性载入函数的优点是只在执行分支代码时牺牲一点儿性能。至于哪种方式更合适,就要看你的具体需求而定了。不过这两种方式都能避免执行不必要的代码。

    1.4、函数绑定

    函数绑定要创建一个函数,可以在特定的 this 环境中 以指定参数调用另一个函数。该技巧常常和回调函数与事件处理程序一起使用,以便在将函数作为变量传递的同时保留代码执行环境。
    请看以下例子:

    var handler = {
      message: 'Event Handled',
      handleClick: function() {
        console.log(this.message)
      }
    }
    
    var btn = document.getElementById('btn')
    btn.onclick = handler.handleClick
    

    当点击按钮的时候,会输出 undefined。问题在于没有保存 handler.handleClick() 的环境,所以 this 对象是指向 DOM 按钮而非 handler

    可以使用一个闭包来修正这个问题

    btn.onclick = function(event) {
      handler.handleClick(event)
    }
    

    在代码中创建多个闭包可能会令代码变得难于理解和调试。因此,很多 JavaScript 库实现了一个可以将函数绑定到指定环境的函数。这个函数一般叫 bind()

    一个简单的bind() 函数接受一个函数和一个环境,并返回一个在给定环境中调用给定函数的函数,并且将所有参数原封不动传递过去。

    function bind(fn, context) {
      return function() {
        return fn.apply(context, argumment)
      }
    }
    

    这个函数似乎非常简单,但其功能是非常强大的。当调用返回的函数时,它会在给定环境中执行被传入的函数并给出所有参数。
    bind() 函数按如下方式使用

    var handler = {
      message: 'Event handled',
      handleClick: function(event) {
        console.log(this.message + ': ' + event.type)
      }
    }
    
    var btn = document.getElementById('btn')
    btn.onclick = bind(handler.handleClick, handler)
    

    ECMAScript 5 为所有函数定义了一个原生的 bind() 方法,进一步简单了操作。换句话说,你不用再自己定义 bind() 函数了,而是可以直接在函数上调用这个方法。
    例如:

    btn.onclick = handler.handleClick.bind(handler)
    

    原生的 bind() 方法与前面介绍的自定义 bind() 方法类似,都是要传入作为 this 值的对象。

    1.5、函数柯里化

    与函数绑定紧密相关的主题是 函数柯里化(function currying),它用于创建已经设置好了一个或多个参数的函数。函数柯里化的基本方法和函数绑定是一样的:使用一个闭包返回一个函数。两者的区别在于,当函数被调用时,返回的函数还需要设置一些传入的参数。
    如下示例:

    function add(num1, num2) {
      return num1 + num2
    }
    
    function curriedAdd(num2) {
      return add(5, num2)
    }
    
    console.log(add(2, 3))
    console.log(curriedAdd(3))
    

    尽管从技术上来说 curriedAdd() 并非柯里化的函数,但它很好地展示了其概念

    柯里化函数通常由以下步骤动态创建:调用另一个函数并为它传入要柯里化的函数和必要参数。下面是创建柯里化函数的通用方式

    function curry(fn) {
    
      var args = Array.prototype.slice.call(arguments, 1) // 获取其他参数
      return function() {
        var innerArgs = Array.prototype.slice.call(arguments) // 当前传入参数
        var finalArgs = args.concat(innerArgs)
        return fn.apply(null, finalArgs)
      }
    }
    

    这个函数并没有考虑到执行环境,所以调用 apply() 时第一个参数是 null。curry() 函数可以按以下方式应用。

    function add(num1, num2) {
      return num1 + num2
    }
    
    var curriedAdd = curry(add, 5, 12)
    console.log(curriedAdd()) // 17
    

    函数柯里化还常常用作函数绑定的一部分包含在其中,构造成更为复杂的bind() 函数。
    例如:

    function bind(fn, context) {
      var args = Array.prototype.slice.call(arguments, 2)
      return function() {
        var innerArgs = Array.prototype.slicr.call(arguments)
        var finalArgs = args.concat(innerArgs)
        return fn.apply(context, finalArgs)
      }
    }
    

    当你想除了 event 对象再额外给事件处理程序传递参数时,这非常有用。
    例如:

    var handler = {
      message: 'Event Handled',
      handleClick: function(name, event) {
        console.log(this.message + ': ' + name + ': ' + event.type)
      }
    }
    
    document.getElementById('btn').onclick = bind(handler.handleClick, handler, 'my-btn')
    

    ECMAScript 5 的bind() 方法也实现函数柯里化,只要在 this 的值之后再传入另一个参数即可。

    var handler = {
      message: 'Event Handled',
      handleClick: function(name, event) {
        console.log(this.message + ': ' + name + ': ' + event.type)
      }
    }
    document.getElementById('btn').onclick = handler.handleClick.bind(handler, 'my-btn')
    

    二、防篡改对象

    JavaScript 共享的本质一直是开发人员心头的痛。因为任何对象都可以被在同一环境中运行的代码修改。ECNAScript 5致力于解决这个问题,可以让开发人员定义防篡改对象(tamper-proof object)
    通过设置对象属性([ [ Configurable ] ]、[ [ Writable ] ]、[ [ Enumerable ] ]、[ [ value ] ]、[ [ Get ] ]、[ [ Set ] ]特性),以改变属性的行为类似,ECMAScript 5 也增加了几个方法,通过它们可以指定对象的行为。不过注意:一旦把对象定义为防篡改,就无法撤销了。

    2.1、不可扩展对象

    默认情况下,所有对象都是可以扩展的。也就是说,任何时候都可以向对象中添加属性和方法。

    var person = { name: 'Nicholas' }
    person.age = 29
    

    使用Object.preventExtensions() 方法可以改变这个行为,让你不能再给对象添加属性和方法(但是可以修改已有的成员)。

    var person = { name: 'Nicholas' }
    Object.preventExtensions( person )
    
    person.age = 29
    console.log(person.age) // undefined
    

    在调用 Object.preventExtensions() 方法后,就不能给 person 对象添加新属性和方法了。在严格模式下,尝试给不可扩展的对象添加新成员会导致抛出错误。

    可以通过 Object.isExtensible() 方法还可以确定对象是否可以扩展。

    var person = { name: 'Nicholas' }
    console.log(Object.isExtensible(person)) // true
    
    Object.preventExtensions(person) 
    console.log(Object.isExtensible(person)) // false
    

    2.2、密封的对象

    ECMAScript 5 为对象定义的 第二个保护级别是 密封对象(sealed object)。密封对象不可扩展,而且已有成员的 [ [ Configurable ] ]特性将被设置为 false。这就意味着不能删除属性和方法,因为不能使用 Object.defineProperty() 把数据属性 修改为访问器属性,或者相反。属性值是可以修改的。

    要密封对象,可以使用 Object.seal() 方法。

    var person = { name: 'Nicholas' }
    Object.seal(person)
    
    person.age = 29
    console.log(person.age) // undefined
    
    delete person.name
    console.log(person.name) // 'Nicholas'
    

    Object.isSealed() 方法可以确定对象是否被密封了。因为被密封的对象不可扩展,所以用 Object.isExtenside() 检测密封的对象也会返回 false。

    var person = { name: 'Nicholas' }
    console.log(Object.isExtensible(person)) // true
    console.log(Object.isSealed(person)) // false
    
    Object.seal(person)
    console.log(Object.isExtensible(person)) // false
    console.log(Object.isSealed(person)) // true
    

    2.3、冻结对象

    最严格的的范篡改级别是 冻结对象(frozen object)。冻结的对象即不可扩展,又是密封的,而且对象数据属性的 [ [ Writable ] ]特性会被设置为 false。如果定义 [ [ Set ] ]函数,访问器属性任然是可写的。ECMAScript 5 定义 Object.freeze() 方法来冻结对象

    var person = { name: 'Nicholas' }
    Object.freeze(person)
    
    person.age = 29 // 创建
    console.log(person.age) // undefined
    
    delete person.name // 删除
    console.log(person.name) // Nicholas
    
    person.name = 'Grey' // 修改
    console.log(person.name) // Nicholas
    

    可以通过 isFrozen()来检测对象是否被冻结,因为冻结对象即使密封的又是不可扩展的,所以用 Object.isExtensible()Object.isSealed() 检测冻结对象将分别返回 false 和 true

    var person = { name: 'Nicholas' }
    console.log(Object.isExtensible(person)) // true
    console.log(Object.isSealed(person)) // false
    console.log(Object.isFrozen(person)) // false
    
    Object.freeze(person)
    console.log(Object.isExtensible(person)) // false
    console.log(Object.isSealed(person)) // true
    console.log(Object.isFrozen(person)) // true
    

    对 JavaScript 库的作者而言,冻结对象是很有用的。因为 JavaScript 库最怕有人意外(或有意)地修改了库中的核心对象。冻结(或密封)主要的库对象能够防止这些问题的发生。

    三、高级定时器

    JavaScript 是运行于单线程的环境中的,而定时器仅仅只是计划代码在未来的某个时间执行。实际上,浏览器负责进行排序,指派某段代码在某个时间点运行的优先级。
    除了 主 JavaScript 执行进程外,还有一个需要在进程下一次空闲执行的代码队列。随着页面在其生命周期中的推移,代码会按照执行顺序添加入队列。例如,当某个按钮被按下时,它的事件处理程序代码就会被添加到队列中

    定时器对队列的工作方式是,当特定时间过去后将代码插入。注意,给队列添加代码并不意味着对它立即执行,而只能表示它会尽快执行。设定一个事件的定时器,表示在这个事件后会被添加到 队列中。

    var btn = document.getElementById('btn')
    
    btn.onclick = function() {
      setTimeout(function() {
        document.getElementById('message').style.visibility = 'visible'
      }, 250)
    }
    

    实际上,对 setTimeout() 的调用表示要晚点执行某些代码。指定的事件间隔表示何时将定时器的代码添加到队列中,而不是何时实际执行代码

    3.1、重复的定时器

    使用 setInterval() 创建的定时器确保了定时器代码规则的插入队列中。当使用 setInterval() 时,仅当没有该定时器的任何其他代码实例,才将定时器代码添加到队列中。这确保了定时器代码加入到队列中的最小时间间隔为指定间隔。

    这种处罚定时器的规则有两个问题:

    1. 某些间隔会被跳过。
    2. 多个定时器的代码执行之间的间隔可能会比预期的小。

    假设,摸个 onclick 时间处理程序使用 setInterval() 设置了一个 200ms 间隔的重复定时器。如果事件处理程序花了 300ms 多一点的事件完成,同时定时器代码也花了差不多的事件,就会出现跳过间隔且连续运行定时器代码的情况。

    为了避免 setInterval() 的重复定时器的这2个缺点,你可以用如下模式 使用链式 setTimeout() 调用。

    setTimeout(function() {
      // 处理中
      setTimeout(arguments.callee, interval)
    }, interval)
    

    这样做的好处是,在前一个定时器代码执行完之前,不会向队列插入新的定时器代码,确保不会有任何缺失的间隔。而且,它可以保证在下一次定时器代码执行之前,至少要等待指定的间隔,避免了连续的运行。这个模式要用于重复定时器,如下例所示:

    setTimeout(function() {
      var div = document.getElementById('myDiv')
      var left = parseInt(div.style.left) + 5
      div.style.left = left + 'px'
    
      if (left < 200) {
        setTimeout(arguments.callee, 50)
      }
    }, 50)
    

    `这段代码每次执行的时候将一个 <div> 元素向右移动,当左坐标在 200像素的时候。停止 JavaScript 动画中使用这个模式很常见

    3.2、Yielding Processes

    运行在浏览器中的 JavaScript 都被分配了一个确定数量的资源,JavaScript被严格限制。其中一个限制时长时间运行脚本的制约,如果代码运行超过特定的时间桌子特定语句数量就不让它继续执行。
    脚本长时间运行的问题常常由两个原因之一造成的:过长的、过深嵌套的函数调用或者是进行大量处理的循环。

    长时间运行的循环通常遵循以下模式:

    for (var i = 0, len = data.length; i < len; i++) {
      process(data[i[)
    }
    

    这个模式的问题在于要处理的项目的数量在运行前是不可知的。数组中的项目数量直接关系到执行完成该循环的时间长度。由于 JavaScript 的执行是一个阻塞操作,脚本运行所花时间越久,用户无法与页面交互的时间也越长。

    在展开该循环之前,你需要回答以下两个重要的问题:

    • 该处理程序是否必须同步完成?
    • 数据是否必须按顺序完成?

    同时对于上诉两个问题,你的回答都是 “否”,那么你可以使用定时器分割这个循环。这是一种叫做数组分块(array chunking)的技术。基本的思路是为要处理的项目创建一个队列,然后使用定时器取出下一个要处理的项目进行处理,接着再设置另一个定时器。基本的模式如下。

    setTimeout(function() {
      // 取出下一个条目并处理
      var item = array.shift()
      process(item)
    
      // 若还有条目,在设置另一个定时器
      if (array.length > 0) {
        setTimeout(arguments.callee, 100)
      }
    }, 100)
    

    要实现数组分块非常简单,可以使用以下函数。

    function chunk(array, process, context) {
      setTimeout(function() {
        var item = array.shift()
        process.call(context, item)
    
        if (array.length > 0) {
          setTimeout(arguments.callee, 100)
        }
      }, 100)
    }
    

    可以按如下所示使用该函数:

    var data = [12, 123, 1234, 12345, 456, 789]
    
    function printValue(item) {
      var div = document.getElementById('div')
      div.innerHTML += item + '<br>'
    }
    
    chunk(data, printValue)
    

    由于函数处在全局作用域中,因此无需给 chunk() 传递一个 context 对象


    须要当心的地方是,传递给 chunk() 的数组是用作一个队列的,因此当处理数据的同时,数组中的条目也在改变。如果你想保持原数组不变,则应该将数组的克隆传递给 chunk(),

    chunk(data.concat(), printValue)
    

    数组分块的重要性在于它可以将多个项目的处理在执行队列上分开,在每个项目处理之后,给与其他的浏览器处理机会运行,这个就可能避免长时间运行脚本的错误

    3.3、函数节流

    浏览器中某些计算 和 处理要比其他的昂贵很多。例如,DOM操作比起非 DOM 交互需要更多的内存和 CPU 时间。onresize 事件处理程序内部如果尝试进行复杂 DOM 操作,其高频率的更改可能会让浏览器奔溃。为了绕开这个问题,你可以使用 定时器对该函数进行节流。

    函数节流背后的基本思想是指,某些代码不可以在没有间断的情况下连续重复执行。

    以下为基本形式:

    var processor = {
      timeoutId: null,
    
      // 实际进行处理的方法
      performProcessing: function() {
        // 实际执行的代码
      }
    
      // 初始处理调用的方法
      process: function() {
        clearTimeout(this.timeoutId)
    
        var that = this
        this.timeoutId = setTimeout(function() {
          that.perfomProcessing()
        }, 100)
      }
    }
    
    // 尝试开始执行
    processor.process()
    

    这个模式可以使用 throttle() 函数来简化,这个函数可以自动进行定时器的设置和清除。
    如下示例:

    function throttle(method, context) {
      cleartTimeout(method.tId)
      method.tId = setTimeout(fuunction() {
        method.call(context)
      }, 100)
    }
    

    throttle() 函数接受两个参数;要执行的函数以及在哪个作用域中执行。


    节流在 resize 事件中时最常用的。如果你基于该事件来改变页面布局的话,最好控制处理的频率,以确保浏览器不会在极端的时间内进行过多的计算。

    function resizeDiv() {
      var div = document.getElementById('myDiv')
      div.style.height = div.offsetWidth + 'px'
    }
    window.onresize = function() {
      throttlel(resizeDiv)
    }
    

    四、自定义事件

    事件时 JavaScript 与浏览器交互的主要途径。事件是一种叫做观察者的设计模式,这是一种创建松散耦合代码的技术。对象可以发布事件,用来表示在该对象生命周期中某个有趣的时刻到了。然后其他对象可以观察该对象,等待这些有趣的时刻到来并通过运行代码来响应。
    观察者模式由两类对象组成:主题和观察者主体负责发布事件,同时观测者通过订阅这些事件来观察该主体。该模式的一个关键概念是主体并不知道观察者的任何事情,也就是说它可以独自存在并正常运行即使观察者不存在。从另一个方面来说,观察者知道主体能注册事件的回调函数(事件处理程序)。涉及DOM上时,DOM元素便是主体,你的事件处理代码便是观察者。

    自定义事件背后的概念是创建一个管理事件的对象,让其他对象监听那些事件。实现此功能的基本模式。
    可以如下定义:

    function EventTarget() {
    
      this.handlers = {}
    }
    
    EventTarget.prototype = {
    
      constructor: EventTarget,
      // 注册事件
      addHandler: function(type, handler) { 
        if (typeof this.handlers[type] == 'undefined') this.handlers[type] = [] // 通过 type 属性创建队列
    
        this.handlers[type].push(handler) // 添加到队列中
      },
      fire: function(event) {
        if (!event.target) event.target = this 
    
        if (this.handlers[event.type] instanceof Array) {
          var handlers = this.handlers[event.type] // 获取对应事件队列
    
          for (var i = 0, len = handlers.length; i < len; i++) { // 依次执行
            handlers[i](event)
          }
        }
      },
      removeHandler: function(type, handler) {
        if (this.handlers[type] instanceof Array) {
          var handlers = this.handlers[type]
          for (var i = 0, len = handlers.length; i < len; i++) { // 迭代寻找,要清楚的事件处理程序
    
            if (handlers[i] == handler) break
          }
          handlers.splice(i, 1) // 从 队列中移除
        }
      }
    }
    

    使用 EventTarget 类型的自定义事件可以如下使用:

    
    function handlerMessage(event) {
      console.log('Message received:' + event.message)
    }
    
    // 创建一个新对象
    var target = new EventTarget()
    
    // 添加一个事件处理程序
    target.addHandler('message', handlerMessage)
    
    // 触发事件
    target.fire({type: 'message', message: 'Hello World!'}) // Message received:Hello World!
    
    // 删除事件处理程序
    target.removeHandler('message', handlerMessage)
    
    // 尝试再次触发事件
    target.fire({type: 'message', message: 'Hello World!'})
    

    这种功能是封装在一个自定义类型中的,其他对象可以继承 EventTarget 并获得这个行为。
    如下例所示:

    function Person(name, age) {
      EventTarget.call(this)
      this.name = name
      this.age = age
    }
    
    inheritPrototype(Person, EventTarget)
    
    Person.prototype.say = function(message) {
      this.fire({type: 'message', message: message})
    }
    

    Person 类型使用了寄生组合继承方法来继承 EventTarget。一旦调用了 say() 方法,便会触发事件,它包含消息的细节。

    在某种类型的另外的方法中调用 fire() 方法是很常见的,同时 它通常不是公开调用的。这段代码可以按照如下方式使用。

    function handleMessage(event) {
      console.log(event.target.name + ' says: ' + event.message)
    }
    
    // 创建新 person
    var person = new Person('Nocholas', 29)
    
    // 添加一个事件处理程序
    person.addHandler('message', handleMessage)
    
    // 在该对象上调用1个方法,它触发消息事件
    person.say('Hi there.')
    

    当代码中存在多个部分在特定时刻交互的情况下,自定义事件就非常有用了。使用自定义事件有助于解耦相关对象,保持功能的隔绝。在很多情况中,触发事件的代码和监听事件的代码是完全分离的。

    五、拖放

    拖放是一种非常流行的用户界面模式。拖放的基本概念很简单:创建一个绝对定位的元素,使其可以用鼠标移动。这个技术源自一种叫做“鼠标拖尾”的几点网页技巧。

    鼠标拖尾:是一个或多个图片在页面上跟着鼠标指针移动。单元素鼠标拖尾的基本代码需要为文档设置一个 onmousemove 事件处理程序,他总是将指定元素移动到鼠标指针的位置。
    如下面的例子所示:

    document.onmousemove = function(event) {
      var myDiv = document.getElementById('myDiv')
      myDiv.style.left = event.clientX + 'px'
      myDiv.style.top = event.clientY + 'px'
    }
    

    它的效果是一个元素始终跟随指针在页面上的移动。


    最简单的拖放界面可用以下代码实现:

    var DragDrop = function() {
    
      var dragging = null
    
      function handleEvent(event) {
        
        // 获取事件 和 目标
        var target = event.target
    
        // 确定事件类型
        switch(event.type) {
          case 'mousedown':
            if (target.className.indexOf('draggable') > -1) {
              dragging = target
            }
            break;
          
          case 'mousemove': 
            if (dragging !== null) {
              
              // 指定位置
              dragging.style.left = event.clientX + 'px'
              dragging.style.top = event.clientY + 'px'
            }
            break;
          
          case 'mouseup':
            dragging = null  
            break;
        }
      }
    
      // 公共接口
      return {
        enable: function() {
          document.onmousedown = handleEvent
          document.onmousemove = handleEvent
          document.onmouseup = handleEvent
        },
    
        disable: function() {
          document.onmousedown = null
          document.onmousemove = null
          document.onmouseup = null
        }
      }
    }();
    // 调用 enable()
    DragDrop.enable()
    

    拖放会自动针对所有包含 “draggable” 类的元素启用(为了元素能被拖放,他必须是绝对定位的。),如下例所示:

    <div class="draggable" style="width: 50px; height: 50px; background-color: cyan;position: absolute;">
    </div>
    

    5.1、修缮拖动功能

    当完成了上面的例子之后,会发现元素的左上角总是和指针在一起。理想情况是,这个动作应该看上去好像这个元素是指针 “拾起”的,也就是说当在拖动元素的时候,用户点击的那一点就是指针应该保持的位置。

    要达到需要的消息,必须在做一些额外的计算。你需要计算元素左上角和指针位置之间的差值。这个差值应该在 mousedown 事件发生的时候确定,并且一直保存,通过将 event 的 clientX 和 clientY 属性与该元素的 offsetLeft 和 offsetTop 属性进行比较,就可以算出水平方向和垂直方向上需要多少空间

    为了保存想x 和 y 坐标上的差值,还需要几个变量。diffX 和 diffY 这些变量需要在 onmousemove 事件处理程序中用到,来对元素进行适当的定位,如下面的例子所示:

    var DragDrop = function() {
    
      var dragging = null,
          diffX = 0,
          diffY = 0
    
      function handleEvent(event) {
        
        // 获取事件 和 目标
        var target = event.target
    
        // 确定事件类型
        switch(event.type) {
          case 'mousedown':
            if (target.className.indexOf('draggable') > -1) {
              dragging = target
              diffX = event.clientX - target.offsetLeft
              diffY = event.clientY - target.offsetTop
            }
            break;
          
          case 'mousemove': 
            if (dragging !== null) {
              
              // 指定位置
              dragging.style.left = (event.clientX - diffX) + 'px'
              dragging.style.top = (event.clientY - diffY) + 'px'
            }
            break;
          
          case 'mouseup':
            dragging = null  
            break;
        }
      }
    
      // 公共接口
      return {
        enable: function() {
          document.onmousedown = handleEvent
          document.onmousemove = handleEvent
          document.onmouseup = handleEvent
        },
    
        disable: function() {
          document.onmousedown = null
          document.onmousemove = null
          document.onmouseup = null
        }
      }
    }();
    

    5.2、添加自定义事件

    前面的代码没有提供任何方法表示拖动开始、正在拖动或已经结束。这时,可以使用 自定义事件来指示这几个事件的发生,让应用的其他部分与拖动功能进行交互。

    由于 DragDrop 对象是一个使用了模块模式的单例,所以需要进行一些更改来使用 EventTarget 类型。

    创建一个新的EventTarget 对象,然后添加 enable() 和 disable() 方法,最后返回这个对象。

    var DragDrop = function() {
    
      var dragdrop = new EventTarget()
          dragging = null,
          diffX = 0,
          diffY = 0
    
      function handleEvent(event) {
        
        // 获取事件 和 目标
        var target = event.target
    
        // 确定事件类型
        switch(event.type) {
          case 'mousedown':
            if (target.className.indexOf('draggable') > -1) {
              dragging = target
              diffX = event.clientX - target.offsetLeft
              diffY = event.clientY - target.offsetTop
              dragdrop.fire({type: 'dragstart', target: dragging, x: event.clientX, y: event.clientY})
            }
            break;
          
          case 'mousemove': 
            if (dragging !== null) {
              
              // 指定位置
              dragging.style.left = (event.clientX - diffX) + 'px'
              dragging.style.top = (event.clientY - diffY) + 'px'
    
              // 触发自定义事件
              dragdrop.fire({type: 'drag', target: dragging, x: event.clientX, y: event.clientY})
            }
            break;
          
          case 'mouseup':
            dragdrop.fire({type: 'dragend', target: dragging, x: event.clientX, y: event.clientY})
            dragging = null  
            break;
        }
      }
    
      // 公共接口
        dragdrop.enable = function() {
          document.onmousedown = handleEvent
          document.onmousemove = handleEvent
          document.onmouseup = handleEvent
        }
        dragdrop.disable =  function() {
          document.onmousedown = null
          document.onmousemove = null
          document.onmouseup = null
        }
    
        return dragdrop
    }();
    

    这些模块中的细小更改令DragDrop 对象支持了事件,如下:

    DragDrop.enable()
    DragDrop.addHandler('dragstart', function(event) {
      var status = document.getElementById('status')
      status.innerHTML = 'Started dragging: ' + event.target.id
    })
    
    DragDrop.addHandler('drag', function(event) {
      var status = document.getElementById('status')
      status.innerHTML = 'Dragged ' + event.target.id + 'at ('+ event.x +', '+ event.y +')'
    })
    
    DragDrop.addHandler('dragend', function(event) {
      var status = document.getElementById('status')
      status.innerHTML = 'Dropped ' + event.target.id + 'at ('+ event.x +', '+ event.y +')'
    })
    

    对应 HTML 结构

      <p id="status" style="text-align: center"></p> <!-- 显示拖放相关信息 -->
    <div class="draggable" id="div" style="width:50px;height:50px;background:cyan;position:absolute;">
    </div>
    

    为 DragDrop 添加自定义事件可以使这个对象更健壮,他将可以在网络应用中处理复杂的拖放功能。

    六、小结

    JavaScript 中的函数非常强大,因为他们是第一类对象。使用闭包和函数环境切换,还可以有很多使用函数的强大方法。可以创建作用域安全的构造函数,确保在缺少 new 操作符时调用 构造函数不会改变错误的 环境对象。

    • 可以使用 惰性载入函数,将任何代码分支推迟到第一次调用函数的时候。
    • 函数绑定可以让你创建始终在指定环境中运行的函数,同时函数柯里化可以让你创建已经填了某些参数的函数。
    • 将绑定和柯里化组合起来,就能够给你一种在任意环境中以任意参数执行任意函数的方法。

    ECMAScript 5 允许通过以下几种方式来创建方篡改对象。

    • 不可扩展的对象,不允许给对象添加新的属性和方法。
    • 密封的对象,也是不可扩展的对象,不允许删除已有的属性和方法
    • 冻结的对象,也是密封的对象,不允许重写对象的成员

    JavaScript 中可以使用 setTimeout() 和 setInterval() 如下创建定时器

    • 定时器代码是放在一个等待区域,直到时间间隔到了之后,此时将代码添加到 JavaScript 的处理队列中,等待下一次 JavaScript 进程空闲时被执行
    • 每次一段代码结束之后,都会有一小段空闲时间进行其他浏览器处理。
    • 可以使用定时器将长时间的脚本切分为一小块一小块可以在以后运行的代码段。这种做法有助于 Web 应用对用户交互有更积极的响应。

          JavaScript 中经常以事件的形式应用观察者模式。虽然事件常常和 DOM 一起使用,但是你也可以通过实现自定义事件在自己的代码中应用。使用自定义事件有助于将不同部分的代码相互之间解耦,让维护更加容易,并减少引入错误的机会。

          拖放对于左面 和 Web 应用都是一个非常流行的用户界面范例,它能够让用户非常方便地以一种直观的方式重新排列或者配置东西。在JavaScript 中可以使用鼠标事件和一些简单的计算来实现这种功能类型。将驼房行为和自定义事件结合起来可以创建一个可重复使用的框架,它能应用于各种不同的情况下。

    相关文章

      网友评论

          本文标题:高级技巧

          本文链接:https://www.haomeiwen.com/subject/nkqrsctx.html