写一个throttle函数

上一篇写了(写一个debounce函数),这篇就写一下throttle函数。

throttle是怎样的一个函数呢?和debounce函数又有什么区别呢?

throttle就是节流阀,控制在一定时间范围内只执行一次函数,其实根据节流阀的名字也可以很容易理解到,水不断来的时候,通过你节流阀,就可以控制你自己的实际流量,单位时间内怎样流出。

举个实际例子,例如我们需要监听窗口的大小变化来改变另一个东西的样式,那么我们肯定监听resize来触发我们的函数,但是实际上,resize触发函数会在一秒内触发很多次,难道一秒内我们就要执行函数这么多次了吗?显示,这不是我们想要的,可能我们想要的只是300ms执行一次就够了。

好了,这就不难理解这个函数做了什么工作了。

废话少说,直接开写,首先肯定需要传入执行的函数,还有我们知道需要控制某个时间内只执行一次,那会有一个interval参数,那先写个开头:

function throttle(fn, interval) {
  let _fn = fn  //保存函数的引用
  return function(){
    let _self = this
    let args = arguments
    setTimeout(function(){
      _fn.apply(_self, args)
    }, interval)
  }
}

这个开头和debounce开头一样,关键就是后面的逻辑了,也是和debounce的区别,我们想要这个函数在某个时间段只执行一次,如果在这个时间段内继续触发函数,则不执行,那么就可以增加代码为:

function throttle(fn, interval) {
  let _fn = fn  //保存函数的引用
  let timer
  return function(){
    //函数还没执行,则直接返回
    if(timer){
      return
    }
    let _self = this
    let args = arguments
    timer = setTimeout(function(){
      _fn.apply(_self, args)
      clearTimeout(timer)
      timer = null  //注意执行完要清空timer
    }, interval)
  }
}

好了,我们发觉,那如果第一次函数被触发呢?不应该直接返回吧,第一次那肯定立即要执行,不用延时吧,ok,再加个标识符:

function throttle(fn, interval) {
  let _fn = fn  //保存函数的引用
  let timer,
      firstTime = true
  return function(){
    let _self = this
    let args = arguments
    //判断第一次进来就立即执行
    if(firstTime){
      _fn.apply(_self, args)
      firstTime = false
      return
    }
    //函数还没执行,则直接返回
    if(timer){
      return
    }
    timer = setTimeout(function(){
      _fn.apply(_self, args)
      clearTimeout(timer)
      timer = null  //注意执行完要清空timer
    }, interval || 500)
  }
}

其实具体的debounce和throttle函数实现起来有很多种写法,但是都大同小异,因为基本原理都是一样的,做了各自不同扩充而已。其实理解debounce和throttle的区别很容易,debounce就相当于以触发动作为参考,延时了设定的时间就执行一次函数,如果动作在设定的时间内仍然继续触发,那么继续以新触发的动作为参考;而throttle就是第一次触发就立即执行了,然后以执行的时间为参考,延时了设定的时间之后再触发就再执行,但是如果下一次触发仍在前一次执行的设定延时时间内,则不执行了,大概,它们就是如此。

写一个debounce函数

这里尝试写一个debounce函数的过程。

debounce是什么?就是防抖动,假设我们监听了输入框的change事件,绑定了一个函数来验证,那么我们在输入的过程中就会不断触发这个函数,很多时候我们其实不想这个函数触发得这么频繁,因为用户都是不断输入的,没必要不断验证,我们真实希望的是,用户停止的时候才触发这个函数,所以我们希望给一个delay的时间,在这个时间内,不断触发的函数,也只按最后一次来触发。

就好像上图那样,只要你触发后面,如果不超过delay时间再次触发的话,也会继续等,直至最后一次触发超过delay的时间,函数才执行一次,很好理解。

当然了,如果希望的是立即执行函数的话,情况就会是下面这种情况:

只要你触发的时间比上次函数执行时间超过了delay的时间,就会被立即执行。

这里写的debounce函数就不考虑立即执行这种情况,按前面的情况来写一下。

首先,这个函数名称就叫做debounce,这个函数接受两个参数,一个是防抖执行的函数,一个是延时delay的时间,所以我们会用到setTimeout这个方法,而且我们执行了debounce之后需要返回一个被触发的函数,考虑到我们需要,会先有:

function debounce(fn, delay) {
  let _fn = fn  //保存函数的引用
  return function(){
    let _self = this
    let args = arguments
    setTimeout(function(){
      _fn.apply(_self, args)
    }, delay)
  }
}

这样执行下来,也只不过每个函数都执行一次延时而已,还没达到我们想要的效果,所以我们还需要做的是,这个函数在delay时间内,也就是还没执行前,再次触发的话,就将上一次设定的延时器清除,那这就容易增加代码为:

function debounce(fn, delay) {
  let _fn = fn  //保存函数的引用  
  let timer
  return function(){
    clearTimeout(timer)
    let _self = this
    let args = arguments
    timer = setTimeout(function(){
      _fn.apply(_self, args)
    }, delay)
  }
}

这就基本实现了debounce函数了,其实就是利用了一个闭包的形式,将原本的timer保存起来,只要函数还没执行(也就是delay时间内)再次触发的话,就会先清除了前一个的timer了,基本实现了防抖的效果。

js创建对象的7种模式

其实我们在新建一个对象的时候,通常会用什么方法呢?我觉得,大多数的人都会这样新建,简单直接:

var myObject = {
  name:'test',
  content:'hello world',
  do:function(){console.log('nothing')}
}

在这里,以上面例子来详细说说js创建对象的7种模式:

第一种:工厂模式

function createObject(name, content){
  return {
    name,
    content,
    do:function(){console.log('nothing')}
  }
}

var myObject = createObject('test', 'hello world')

这种模式非常直观,丢进去,丢出来。

第二种:构造函数模式

function createObject(name, content){
  this.name = name
  this.content = content
  this.do = function(){console.log('nothing')}
}

var myObject = new createObject('test', 'hello world')

这种模式也很常用,定义了一个构造函数,然后对象都是new出来的。

第三种:原型模式

function createObject(){
}
createObject.prototype.name = 'test'
createObject.prototype.content = 'hello world'
createObject.do = function(){console.log('nothing')}
var myObject = new createObject()

这种模式相对构造函数模式,是在它的原型上直接创建属性,后面new出来的对象就会继承下来这些属性和方法。

第四种:构造函数模式和原型模型组合

function createObject(name, content){
  this.name = name
  this.content = content
}
createObject.prototype = {
  constructor: createObject,
  do:function(){console.log('nothing')}
}
var myObject = new createObject('test', 'hello world')

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性,结果,每个实例都会有一个自己的一份实例属性和方法,但同时共享着对方法的引用,最大限度地节省了内存,是目前使用最广泛的一种创建自定义类型的方法。

第五种:动态原型模式

function createObject(name, content){
  this.name = name
  this.content = content
  if(typeof this.do != "function"){
    createObject.prototype.do = function(){console.log('nothing')}
  }
}

var myObject = new createObject('test', 'hello world')

这里就是判断是否存在do这个方法前提下动态在原型上添加,而且只在调用函数的时候才会执行,可以说是非常好的一种模式。

第六种:寄生构造函数模式

function SpectialArray(){
  var values = new Array()
  values.push().apply(values, arguments)
  values.toPipedString = function(){
    return this.join('|')
  }
  return values
}

var colors = new SpecialArray('red', 'blue', 'green')
alert(colors.toPipedString())  //"red|blue|green"

其实这种模式也很容易理解,我们不能直接修改Array构造函数,就借用来寄生一下,添加我们需要的属性。

第七种:稳妥构造函数模式

function createObject(name){
  var o = new Object()
  o.do = function(){console.log(name)}
  return o
}
var myObject = createObject('name')

看起来有点熟对吧,其实不完全熟,它其实是通过myObject的方法才能访问到数据成员的这样一种方式,没有其他方式能够访问,myObject就是保存着一个稳妥的对象。

好了,关于js创建对象的7种模式,大抵如上。

Content-Type

说Content-Type之前,先说一下MIME,什么是MIME呢?

百科一下,MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型,是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开,它是一个互联网标准,这个标 准被定义在RFC 2045、RFC 2046、RFC 2047、RFC 2048、RFC 2049等RFC中。 MIME改 善了由RFC 822转变而来的RFC 2822,这些旧标准规定电子邮件标准并不允许在邮件消 息中使用7位ASCII字符集以外的字符。正因如此,一些非英语字符消息和二进制文件 ,图像,声音等非文字消息原本都不能在电子邮件中传输(MIME可以)。MIME规定了用 于表示各种各样的数据类型的符号化方法。 此外,在万维网中使用的HTTP协议中也使 用了MIME的框架,标准被扩展为互联网媒体类型。

前面的文字啰嗦了一点,简单来说,MIME就是表示的一种类型,内容由两部分组成, 前面是数据的大类别,后面定义具体的种类。例如,image/jpeg就是表示jpeg图像类型。

而应用在HTTP中,MIME类型被定义在请求头和响应头的headers中,用Content-Type字段表示,所以我们可以知道HTTP中,Content-Type表示的是一种MIME类型。

响应(response)的头信息,Content-Type表示的是返回的文件类型,这个容易理解,那请求(request)头信息上面的Content-Type呢?代表的是什么呢?

一般,我们在POST/PUT方法的时候,会指定请求头的Content-Type,会有那么的几种类型:

application/x-www-form-urlencoded:这种是最常用的类型,其实就是key=value形式的字符串拼接,例如:key1=value1&key2=value2&key3=value3

multipart/form-data:这种在自己new Formdata()的时候,或者上传文件的时候会用到,会有特别分隔字符串,然后跟着name是什么,对应的value是什么,里面是文件的,还有另一个Content-Type来表明类型,例如:

// request body
–Boundary———————–
content-disposition: form-data; name=”field1″

value1
–Boundary———————–
content-disposition: form-data; name=”field2″

value2
–Boundary———————–
Content-Disposition: form-data; name=”file”; filename=”chrome.png”
Content-Type: image/png
Content-Transfer-Encoding: binary

$binarydata
–Boundary———————–
Content-Disposition: form-data; name=”file1″; filename=”a.txt”
Content-Type: text/plain

application/json:这种类型是现在很常用的一种,用于传递json结构的数据,很适合前后端分离的数据传递。

text/xml:XML-RPC(XML Remote Procedure Call),是一种使用 HTTP 作为传输协议,XML 作为编码方式的远程调用规范。xml相比json太过臃肿,一般用application/json会更加灵活。

 

 

BOM

BOM,不是炸弹爆炸。

BOM,全称是Browser Object Model,即浏览器对象模型,前面(dom的几个级别)已经说过DOM了,那么BOM又是什么来的呢?

其实BOM到现在,还没有正式的标准,但是现在的浏览器在实现js交互方面都提供了相同的方法和属性,所以这些会被认为是BOM的方法和属性。

一张简单的图:

从图里面大概可以认清楚什么是BOM,什么是DOM,和它们之间的关系。

这里就举些简单的例子:

我想获得浏览器窗口的高度:window.innerHeight

我想获得可用的屏幕宽度:screen.availWidth

我想获得当前页面的url:location.href

我想跳转到页面历史的前一个url:history.back()

我想获得浏览器类型:navigator.userAgent

上述等等,其实就是各大对象包含的一些方法和属性,有空就打开调试窗口,打印看看,就什么都知道了。

dom的几个级别

关于DOM的几个级别,其实都是来源于W3C标准,W3C就是World Wide Web Consortium的缩写,中文翻译过来就是万维网联盟,是Web技术领域最具权威和影响力的国际中立性技术标准机构,W3C标准其实包含一系列标准(结构、表现、行为),具体就不展开了。

其中DOM就是它的标准之一,DOM分为了三个级别:DOM1级、DOM2级、DOM3级。

那DOM具体是什么东西呢?DOM其实是文档对象模型(Document Object Model)的缩写,它是一种提供对文档访问或修改方法的模型,它的范围很广,但是对于web开发者来说,往往都认为它是指javascript在浏览器上访问和修改html文档的一种技术,但是实际上它的范围远不局限在这里。

DOM1级(DOM Level 1)于1998年10月成为W3C的推荐标准,DOM1级由两个模块组成:DOM核心(DOM Core)和DOM HTML,DOM Core能映射以XML为基础的文档结构,允许获取和操作文档的任意部分,而DOM HTML通过添加HTML专用的对象与函数对DOM Core进行了扩展。简单来说,DOM1级就是映射文档结构和提供基本的文档操作方法。

DOM2级,就是对DOM1级进行扩展,2级DOM通过对象接口增加了对鼠标和用户界面事件(DHTML长期支持鼠标与用户界面事件)、范围、遍历(重复执行DOM文档)和层叠样式表(CSS)的支持。同时也对DOM 1的核心进行了扩展,从而可支持XML命名空间。简单来说,DOM2级就是在DOM1的基础上增加了视图、事件、样式、遍历和范围的接口,和支持XML命名空间。

DOM3级,在前面DOM的基础上,引入了以统一方式加载和保存文档的方法,新增了验证文档的方法,同时也对DOM核心进行了扩展,开始支持XML 1.0规范。

除了上面的三个等级之外,还有一个叫做DOM0级的东西,实际上标准并没有这个东西,它指的是IE4和Netscape Navigator 4.0最初支持的DHTML,DHTML实际上是HTML、CSS和JS的一个集成,代表的是一种已有的技术,不是标准,所以DOM0级其实代表的是历史节点中未形成标准的一个初期产物。

举个常见的DOM0级事件和DOM2级事件的比较:绑定的按钮的onclick赋值为一个函数就是DOM0级的,但是onclick多次赋值不同函数,最后也会被后面的函数覆盖掉;而DOM2级中利用提供的addEventListener方法监听按钮的click事件,多次写监听同一个事件,函数会被依次执行的,不会被覆盖。

关于DOM几个级别的具体内容,还得细看。

关于Node.js中的模块机制

        为什么要模块,因为它对于代码的复用和解耦有着很关键的作用,关于node.js中的模块加载系统,其实是一个很简单的东西,它是 CommonJS规范的一种实现。所以在说Node.js的模块加载机制之前,先说一说javascript的三种常见的模块化规范:1、CommonJS;2、AMD;3、CMD。

        不拐弯,直入主题,先说CommonJS,它对于javascript模块化有着很重要的作用,它的模块标识是module,模块导出接口是exports,而对于引入模块,只需要一个非常简单的全局性方法require,好了,举个简单的例子:

        先写一个模块文件 calculator.js,内容如下(只是举例,写法不严谨):

// calculator.js 
function plus(a,b){
    return a+b;
}

function minus(a,b){
    return a-b;
}

function times(a,b){
    return a*b;
}

function divide(a,b){
    return a/b;
}

module.exports = { 
    plus,
    minus,
    times,
    divide,
}

        再建一个test.js 的文件,它会引入 calculator这个模块,内容如下:

// 引入calculator模块
var calculator = require('calculator');

//使用calculator模块的plus方法
var plusResult = calculator.plus(1,2);

//打印结果
console.log(plusResult);   // 3

        就是非常简单的用法,关于它实现会在最后详说。

        好了,现在就到说说AMD规范了,为啥会出现这个规范呢?CommonJS不就够了吗?那是因为,CommonJS的require方法引入模块是同步的,如果我放在前端的话,那就非常不适合了,特别文件如果要同步加载,等这么久时间是非常影响用户体验的。所以CommonJS是适用于服务器端的,所以在前端看来,就需要像AMD这种异步模块加载的方式了。

        AMD加载模块也是require方法,但是由于异步,所以多了一个回调参数,对于上面的例子,加载的方法就变为大概这样:

// 引入calculator模块
require(['calculator'],function(calculator){

    //使用calculator模块的plus方法
    var plusResult = calculator.plus(1,2);

    //打印结果
    console.log(plusResult);   // 3
});

        require的第一个参数是数组,传入的是不同的模块,而第二个就是回调函数,异步加载模块后就会执行。RequireJS就是AMD规范的一个典型实现代表。模块的加载路径需要在头部先用require.config来配置。

        而AMD的模块导出方法,就要用到define了,将前面例子改为大概如下:

// calculator.js 
function calculator(){
    var plus = function(a,b){
        return a + b;
    }
    return {
        plus:plus
    }
}

define(calculator);

        当然了,如果这个模块也依赖其他模块,就在define的第一个参数传入模块数组,第二个参数就是回调参数了,和加载形式差不多,其实都是类似的,只不过改了一下写法而已。

        至于CMD,其实和AMD差不多,关于CMD,SeaJS就是它的实现,至于RequireJS和SeaJS两者的差别,可以参考https://github.com/seajs/seajs/issues/277

        好了,最后要回过头来说Node.js的模块机制了。

        因为Node.js的模块机制就是CommomJS的一种实现,所以很多时候我们都可以看到这样的引用:

// 读取一个文件内容
const fs = require('fs');
var content = fs.readFileSync('./book.js',{encoding:'utf8'});

        而模块的导出,有两种方式:

//example.js文件
module.exports = {name:"jack"};   // 方式一
exports.name = "jack";    // 方式二

        引入文件之后,可以用example.name获得模块的输出内容。但是,我们不能写成这种形式:

exports = {name:"jack"};

        因为本来模块的exports属性绑定在module.exports上的,如果你这样写,就等于改写了exports属性了,所以不能导出模块内容了,所以我们可以明确知道,module.exports才是模块真正的导出接口,忌用exports属性来导出模块,至于理解它们两者的关系,它们的假设实现为:

function require(/* ... */) {
    const module = { exports: {} };
        ((module, exports) => {
        // 模块代码在这。在这个例子中,定义了一个函数。
        function someFunc() {}
        exports = someFunc;
        // 此时,exports 不再是一个 module.exports 的快捷方式,
        // 且这个模块依然导出一个空的默认对象。
        module.exports = someFunc;
        // 此时,该模块导出 someFunc,而不是默认对象。
    })(module, module.exports);
    return module.exports;
}

        我们可以看到,我们用require引入模块的时候,里面一开始定义了这样一个变量const module = { exports: {} },而最后返回的是module.exports属性值,所以可以轻易理解为这个就是模块的出口,而中间执行了一段函数,就是将实际模块引入,而模块内的内容,就有module和exports两个形参,如果你改变了exports这个值,那么它不会对原来的module有任何影响,但是你用module.exports就对原来module有插入值的作用,当然,你用exports.property = something也会对原module有插入值的作用。如果你想同时用这两个方式:

module.exports = {name:"Jack"};   // 方式一
exports.name = "Kate"   // 方式二

        根据上面的假设实现,在模块代码中,我们也可以知道,module.exports已经定义了模块的出口内容,而exports.name就不会再有意义了。

        好了,最后说一下Node.js的require加载方式,原生模块直接写模块名称,而自定义模块就要写相对路径了,当然,加载后它都会缓存起来的,它的逻辑在网上找个图就表示很清楚了,如下:

 

文章在我的github上的地址:点击跳转

原创文章,转载请注明出处!

知识共享许可协议
本文章采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行许可。

再说一下javascript中的this

        QQ好友问了我一个问题,如下:

//例子一
var a = document.write;
a('hello');

//例子二
var b = console.log;
b('hello world');

        他问我,为什么例子一会报错,而例子二却是正常输出的呢?

        其实我一看到这样的写法,就知道这是一个坑,你将别的对象的方法赋值给自己,那如果这个方法里面有this的指向,那这样调用的话,this指向谁了,里面又继续调用了什么方法了,所以肯定会报错的。虽然我没看到源代码的这个方法是怎样写的,但是可以写几个例子就可以验证了,这样:

//第一种写法
var a = document;
a.write('hello');

//第二种写法
var a = document.write.bind(document);
a('hello');

//第三种写法
var a = document.write;
a.call(document,'hello');

        上面三种写法都可以达到想要的效果,运行没有问题。所以很容易知道这是this指向的问题,虽然不知道内部机制如何实现这个方法,但是我们在借用某个对象的方法的时候,记得绑定this的指向,不然容易出错。

        好了,对于上面的例子一中document.write方法会出错,那例子二中console.log为什么不会出错呢?

        我认为就是例子二中console的log方法里面没有用到this的指向,可以举个例子:

var example = {
    test:function(str){
        return str+' what?';
    }
}

var a = console.log.bind(example);
a('123');

        上面这个例子中,我将log方法中的this指向改为指向了一个乱来的example,它仍然可以打印出123,所以可以认为log中没有用到this的指向,所以上面例子二是妥妥的。

原创文章,转载请注明出处!

知识共享许可协议
本文章采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行许可。