Skip to content

Latest commit

 

History

History
182 lines (120 loc) · 13.7 KB

20140804.md

File metadata and controls

182 lines (120 loc) · 13.7 KB

阿里前端学习笔记(五)

2014-08-04 by niuzhixiang

##JavaScript中的数组##

###创建数组###

有两种创建数组的方式:

  • 通过构造函数:

      //使用无参构造函数
      var a = new Array();
      //创建数组时指定数组长度
      var a = new Array(5);
      //创建数组并对数组元素进行初始化
      var a = new Array(1,2,3);
    
  • 通过字面量:

      //创建空数组,相当于调用无参构造函数
      var a = [];
      //创建数组并对数组元素进行初始化
      var a = [1,2,3]
    

###数组的索引###

数组元素一般通过下标索引来获取,例如a[1]这样,这很好理解。其实本质上,数组也是对象,通过下标索引访问数组元素实质上是访问数组对象的属性,例如a[1]其实是访问数组对象a的属性“1”,也即a.1(不过由于JavaScript中不允许属性名是数字,因此不能显式地使用a.1这种写法,但是是这个道理)。

有了上面的概念,就很好理解JavaScript中数组的这些特性了:

  • 数组索引可以是负数,例如a[-2]是合法的,它相当于在数组对象a的属性“-2”上进行操作。
  • 数组索引可以不连续,例如创建了一个长度为3的数组var a = ['a', 'b', 'c'],可以直接对它的第11个元素赋值:a[10]='j',因为下标索引仅仅是属性名而已,现在数组对象a具有属性“0”、“1”、“2”和“10”。注意,此时从a[3]到a[9]的值都是undefined,并且此时a.length为11(a.length的值是由a中最大下标索引值加1得到的)。
  • 数组索引可以是字符串,例如a['name']是合法的,它相当于为数组对象a设置了一个属性name,可以通过a.namea['name']两种方式来访问该属性。

###数组元素的添加/删除操作###

  • 最直接最原始的添加/删除操作就是用对象属性的方法对元素进行操作。例如添加一个元素:a[1]=3,删除一个元素:delete a[1]
  • Array提供了对数组元素更方便的操作方式,首先是以栈的方式(先进后出,FILO):push()函数用于向数组末尾添加元素(入栈),pop()函数用于删除数组末尾的元素(出栈)。
  • Array还提供了以队列的方式(先进先出,FIFO)对数组元素进行操作:push()函数(与栈方式的push()函数相同)用于向数组末尾添加元素(入队),shift()函数用于删除数组第一个元素(出队),此外还有unshift()函数用于向数组开头添加元素。
  • 数组元素添加、删除操作的终极神器——splice()函数,它的第一个参数是开始索引,第二个参数是要删除元素的个数,第三个(包括之后)的参数是新插入的元素,例如对于数组b = [1,2,3,4,5,6,7],执行b.splice(2,2)后b变成[1,2,5,6,7],再执行b.splice(2,0,'a')后b变成[1,2,'a',5,6,7]
  • 注意,第一种最原始的添加/删除操作并不会影响其他元素的下标索引及数组的长度length,而Array提供的函数会相应地改变所有数组元素的下标值及数组长度,例如执行shift()函数后,第一个元素被删除,后面的所有元素下标减1,且数组长度length减1。

###数组常用方法###

  • join()函数,用于将数组元素拼接成字符串,例如数组a=[1,2,3]执行a.join('--')后输出1--2--3
  • slice()函数,用于返回数组中的一个片段(子数组),例如数组a=[1,2,3,4,5]执行a.slice(2,4)后输出子数组[3,4]。注意slice()splice()不要混淆,前者用于截取数组片段,后者用于删除或添加数组元素。
  • concat()函数,用于拼接数组,例如对于数组a=[1,2,3]b=[4,5],执行a.concat(b)返回[1,2,3,4,5]
  • reverse()函数,用于倒转数组。注意,该函数会修改原数组(前面几个方法都不会修改原数组,而是会生成一个新数组)。
  • sort()函数,用于对数组进行排序,默认按照字母表升序排序,例如数组a=[1,2,3,10]执行a.sort()之后会返回[1,10,2,3]。此外,还可以向sort()方法传入一个自定义的排序函数(遵循策略模式)。

###参考资料###

参见这里

##前端优化准则##

###尽可能减少HTTP请求数###

减少HTTP请求数的方式包括:将一个页面中引用到的JS文件和CSS文件合并;采用CSS sprites将页面上的背景图合并;将首页的CSS和JS代码直接写到页面里面(内联);将小图片采用base64编码直接写在页面里,等等。

###使用内容分发网络CDN###

将JS和CSS等资源文件分发到CDN各个节点的缓存服务器上,用户访问时就近从离自己最近的节点上获取文件,可以减少网络传输时间,提高速度。

###使用浏览器缓存机制###

服务端可以向HTTP响应头信息中添加一些字段来启用缓存机制,这样浏览器在第一次访问并下载了资源文件后,会将资源文件缓存在本地一段时间,于是以后的若干次访问浏览器就无需每次都再从服务器下载了,而是直接从缓存中读取即可。这样做减少了网络请求,也加快了页面加载速度。带有缓存信息的HTTP响应头如下所示:

HTTP/1.1 200 OK
Date: Fri, 30 Oct 1998 13:19:41 GMT
Server: Apache/1.3.3 (Unix)
Cache-Control: max-age=3600, must-revalidate
Expires: Fri, 30 Oct 1998 14:19:41 GMT
Last-Modified: Mon, 29 Jun 1998 02:28:12 GMT
ETag: “3e86-410-3596fbbc”
Content-Length: 1040
Content-Type: text/html

###压缩CSS和JS代码###

淘宝前端的grunt uglify任务就是对CSS和JS文件进行压缩,生成*-min.js和*-min.css的文件,从而减小网络流量。

###将CSS放在页面最上方###

这样做是因为很多浏览器在CSS文件下载完之前不会渲染任何内容(原因在于CSS是可以覆盖的,高级别的内联CSS可以覆盖低级别的外部CSS,于是浏览器干脆等到所有CSS代码都下载完之后再进行渲染,避免返工)。如果把CSS文件放在页面底部,会导致过一段时间后(加载完之前的HTML代码,并下载完所有CSS文件后)浏览器才会渲染页面,这段时间内会产生空白页的效果。而将CSS文件放在页面顶部可以避免这种情况,不会导致空白页效果。

###将JS放在页面最下方###

这样做是因为JS代码在执行的时候会霸道地阻塞页面其他部分的下载和渲染(原因在于JS代码有可能会操作DOM元素、修改页面内容、进行页面跳转等,因此浏览器干脆在执行JS代码的时候停止其他一切工作,避免返工或者做无意义的工作)。因此,把JS代码放在页面最后执行,可以确保页面其他部分的加载和渲染不会被JS代码的执行所阻塞。

此外浏览器在下载JS文件时会暂停其他资源的下载,将JS文件放在页面最后可以防止因JS文件的下载而拖慢页面其他部分的下载。

###把JS和CSS代码独立成外部文件###

这样做可以减少HTML文件的体积,并且如果使用缓存的话,这些外部JS和CSS文件就不必重复下载了。

###减少DNS解析###

DNS解析从域名到IP地址的转换也是要花费时间的,因此尽量不要在同一个页面中引用太多不同域名下的文件。

###使用Gzip压缩###

把文件先在服务端采用Gzip压缩,然后再传输。浏览器会对接收到的压缩文件进行解压缩,再去解析和执行,这样做可以减少网络流量。目前各主流浏览器和搜索引擎都支持Gzip。

###参考资料###

参见这里

##JavaScript中的执行上下文、作用域链、活动对象、闭包##

###执行上下文、作用域链、活动对象###

直接看代码:

var name=”carney”;   
function introduct(){   
    var lastName=”lee”;   
    function say(){   
        var name=”blank”   
        alert(“my name is “+name+” “+lastName);   
        }   
    say();   
    }   
introduct(); 

这段代码包含了三个执行上下文(全局执行上下文、函数introduct()的执行上下文、函数say()的执行上下文)。

  • 对于全局执行上下文,它的作用域链中只包含一个活动对象,就是全局对象window。
  • 对于函数introduct()的执行上下文,它的作用域链中包含两个活动对象:位于最前端的是当前活动对象,其后是全局活动对象window。
  • 对于函数say()的执行上下文,它的作用域链中包含三个活动对象:位于最前端的是当前活动对象,其后是其外部函数introduct()的活动对象,最后是最外层的全局活动对象window。

活动对象包含了其所处作用域中的变量和函数。作用域链则有序地连接各级活动对象,从而保证对变量和函数的有序访问。

当JS访问一个变量或函数时,会首先在当前执行上下文的活动对象中寻找;如果未找到,则沿着作用域链寻找外层执行上下文的活动对象;如果还未找到,则继续沿着作用域链寻找...最终会到达作用域链的末端,即全局活动对象window;如果仍未找到,则返回undefined。

下图表明了上例代码中的各个执行上下文,以及每个执行上下文对应的作用域链和活动对象。

###闭包###

一般来说,定义在一个函数内部的函数就是一个闭包。通过闭包可以在函数外部访问到函数内部定义的变量,例如以下代码:

var outer = function(){   
    var a = 0;   
    function closure(){   
            console.log(a);   
        }   
    return closure;   
};   
outer()();

以上这段代码中,在外部作用域(全局作用域)中调用outer()函数也能访问到函数内部作用域中定义的变量a,这就归功于闭包closure。

然而要慎用闭包,因为闭包可能会导致内存泄漏。因为正常情况下,当执行完一个函数后,它的活动对象就会被销毁,也就是说该函数内部定义的变量(例如上例中的a)会被销毁。然而如果使用了闭包,由于闭包中引用了外部函数中定义的变量a,那么只有在闭包销毁以后,它所引用的变量a才会被销毁。因此,使用闭包时有些变量不会被及时销毁,导致内存占用过多,从而引起内存泄漏。

##JavaScript性能优化##

  • **尽量使用var声明变量,慎用全局变量。**由于全局变量一般定义在较外层的执行上下文环境中,因此访问全局变量需要搜索更长的作用域链,并且其生命周期更长,不利于内存释放。
  • **确实需要使用全局变量时,应对其进行局部缓存。**将全局变量赋给当前执行环境中的局部变量,可以在重复访问时减少对全局变量的访问次数,提高效率。
  • **避免使用with()函数。**因为with()函数会在当前执行上下文的作用域链顶端创建一个新的活动对象(with活动对象),这会导致作用域链中原来的活动对象都往后挪了一位,从而使变量的访问代价提高。
  • 把经常使用且不常变化的属性和方法直接定义在构造函数的原型对象prototype上。这样做可以避免属性和方法的重复定义。
  • **慎用闭包。**由于闭包会导致外层函数中定义的变量不会随着函数执行完毕而销毁,所以可能会引起内存泄漏。
  • **避免使用getter/setter。**因为JS中对象的所有属性都是对外可见的,因此getter/setter是没有意义的。
  • **避免在循环中使用try/catch。**而是应该把try/catch块放在循环外层。因为try/catch块会对性能有一定影响,应减少try/catch的次数。
  • **使用for代替for...in...遍历数组。**for...in...循环的性能比for循环要低一些。
  • **减少JS的DOM操作。**在使用JS修改DOM元素样式的时候,不要逐条地修改其CSS属性,而应该通过设置其className,一次性应用所有的样式,这样只会触发一次reflow。此外,还应避免遍历大量元素,同时优化节点修改等。
  • **进行事件优化。**当存在多个元素需要注册同一个事件及其回调时,不要重复地在每一个元素上绑定,而应在这些元素的父节点统一地绑定(由于事件冒泡机制,子节点触发的事件如果没有得到处理,会向上传播到父节点进行处理)。
  • **慎用动画。**动画不是为了炫技,而是应确实改善用户体验。并且对于动画元素,应该设置其position属性为absolute或fixed,这样动画元素就会从普通文档流中脱离,执行动画效果时不会影响到普通文档流中的元素布局,因此不会造成reflow。
  • 更多关于JavaScript性能优化的资料请参见这里

##JavaScript中的arguments、caller、callee、call和apply##

  • **arguments:**它是函数内部的一个隐藏对象,它不是数组,但是可以用类似数组的方式进行访问,例如arguments[0]表示函数的第一个参数。注意,arguments必须使用在函数内部才有意义。
  • **caller:**使用方式是functionName.caller,它表示函数functionName的调用者,即调用了functionName这个函数的对象。
  • **callee:**使用方式是arguments.callee,它表示arguments所属的这个函数,即当前正在执行的函数对象。
  • **apply()和call():**它们的作用都是将函数绑定到一个对象上去执行,也就是把函数作为这个对象的方法来执行。两者只是在传参方式上有所区别:functionName.call(obj, arg1, arg2)functionName.apply(obj, argArray)
  • 更多资料参见这里