Quantcast
Channel: IT瘾javascript推荐
Viewing all 148 articles
Browse latest View live

JavaScript 函数、作用域和继承

$
0
0

关于函数、作用域和继承,可以写的非常多。不过和 JavaScript 类型浅解一样,是写给初学者看的,我们着重从简单的来。当然,即使用「简单」来描述,这也是 JavaScript 中最不容易懂的点之一。

一、函数

如你所见, function fn(){},像这个声明式函数。这几乎是函数最常见的内容。一个函数可以有多种形式。虽然初学看起来有点乱,不过我想说的是,你总会知道如何用的,现在知道就可以了。它们除了在 hositing(后面说)表现会有一些不同,其他的都差不多,后面你总会知道如何合理地使用他们的。

1. 声明/匿名函数

当声明函数没有 name的时候,便成了匿名函数。作为 first-class object,像其他对象一样,它也可以作为值传递给一个变量,作为函数的参数。

function [name]([arg1] [, arg2] [..., argN]) {
     statements
}

在 ES6 中,我们可能会使用像 Coffee Script 中类似的语法:

([arg1] [, arg2]) => {
   statements
}

2. Function构建函数

new Function([arg1] [, arg2] [..., argN], statement);

上面的代码表现形式,是壳。而理解函数的重点,还在于里面。这个里面指的是用 {}包起来的一切。在这个花括号乱起来的地方 —— 函数体。以上面的形式为例,我们可以直接使用的有:

  • 参数:可以像变量( argN)一样在函数体中使用
  • 函数名:可以用函数名或者 arguments.callee调用自身
  • 上下文:每一个函数体,都是一个作用域, this代表当前作用域上下文
  • 参数对象: arguments对象按顺序存储着函数变量

让我们直观一点,用一个代码片断来描述上面的内容:

function sofish(gender, age) {
  console.log(gender, age); // male, 28
  console.log(sofish === arguments.callee) // true
  console.log(this); // window
  console.log(arguments); // ['male', 28]
}

二、作用域

作用域可以这样理解 —— 封闭的,不影响外部的空间。不过在 JavaScript 函数可以嵌套,也就是说作用域也可以是多级的。为了更好也把使用域联系起来,它使用了一个在多数现代码语言中都实行的理念 —— 闭包。闭包可以用一句话来理解:外部不能访问内部变量,内部可以外部变量。或者考虑一下如下代码:

function sofish(){

  var nickname = ‘小鱼’;
  
  (function ciaocc() {
    var age = 24;
    
    // 内部函数可以访问外部 sofish() 创建作用域的变量
    console.log(nickname); // '小鱼'
  })();
  // 但是外部函数,不能访问内嵌函数 ciaocc() 创建使用域的变量
  console.log(age); // undefined
}

从作用域的类型看来,总的来说,在 JavaScript 中基本上只有两种形式的使用域 —— 全局/函数级使用域。换句话说,除了全局作用域,只有函数级作用域,像 ifwhilefor...in等代码块是不会创建作用域的。你可以试试运行这段代码:

var sofish = '小鱼'; 

while(sofish === '小鱼') {
  var cc = 'ciaocc';
  sofish = 'whatever';
}

console.log(cc); // 'ciaocc'

说到这里,来理解个非常重要的知识点 —— hoisting。像上面的代码都是按顺序执行的,在同一个作用域,这是比较容易理解的。不过,考虑一下下面这段代码的执行结果:

console.log(typeof sofish); // 1___
console.log(momo); // 2___
function sofish() {
  var ciaocc = 'a beauty';
};
sofish();
var momo;
console.log(ciaocc); // 3___

第 1 个输出的是 'function',第 2 个输出的是 undefined,但为什么最后一个是 Error —— ReferenceError: ciaocc is not defined。结果似乎有点出乎意料。这里面涉及到一个概念 —— hoisting,即函数和变量在代码解析的时候,会提到作用域的最顶端,如上面的代码,其实执行的时候机器看到的代码是这样的:

// 函数和变量被提到最顶端
function sofish() {
  var ciaocc = 'a beauty';
};
var momo;

console.log(typeof sofish); // 1___
console.log(momo); // 2___

sofish();
console.log(ciaocc); // 3___

不过,函数和变量只会被提到 所在作用域的最顶端,因此上面的函数 sofish()中的变量 ciaocc只存在所在作用域顶端,不会提到外部的顶端。那么函数和变量哪个在最顶部呢?考虑一下下面的代码:

console.log(sofish);

function sofish() {};
var sofish = 'ciao cc';

三、继承与构造函数

这里推荐几篇让人更容易理解的文章,深入浅出,我觉得写的非常好:

全篇讲的比较散,有问题的话,可以自己看看下面推荐的一些文章,或者留言之类,虽然基本上是写给某位看的,不过既然分享出来,我也会抽空回答一些问题。


Javascript网页截屏的方法

$
0
0

最近我在研究开发一个火狐插件,具体的功能是将网页内容截屏并分享到微博上。目前基本功能已经实现,大家可以在 @程序师视野里看到用这个截图插件分享的微博的效果。

之前我曾写过 如何将canvas图形转换成图片下载canvas图像的方法,这些都是在为这个插件做技术准备。

技术路线很清晰,将网页的某个区域的内容生成图像,保持到canvas里,然后将canvas内容转换成图片,保存到本地,最后上传到微博。

我在网上搜寻到 html2canvas这个能将指定网页元素内容生成canvas图像的javascript工具。这个js工具的用法很简单,你只需要将它的js文件引入到页面里,然后调用 html2canvas()函数:

html2canvas(document.body, {
    onrendered: function(canvas) {
        /* canvas is the actual canvas element,
           to append it to the page call for example
           document.body.appendChild( canvas );
        */
    }
});

这个 html2canvas()函数有个参数,上面的例子里传入的参数是 document.body,这会截取整个页面的图像。如果你想只截取一个区域,比如对某个 div或某个 table截图,你就将这个 div或某个 table当做参数传进去。

我最终并没有选用html2canvas这个js工具,因为在我的实验过程中发现它有几个问题。

首先,跨域问题。我举个例子说明这个问题,比如我的网页网址是http://www.webhek.com/about/,而我在这个页面上有个张图片,这个图片并不是来自www.webhek.com域,而是来自CDN图片服务器www.webhek-cdn.com/images/about.jpg,那么,这张图片就和这个网页不是同域,那么html2canvas就无法对这种图片进行截图,如果你的网站的所有图片都放在单独的图片服务器上,那么用html2canvas对整个网页进行截图是就会发现所有图片的地方都是空白。

这个问题也有补救的方法,就是用代理:

<!DOCTYPE html><html><head><meta charset="utf-8"><title>html2canvas php proxy</title><script src="html2canvas.js"></script><script>
        //<![CDATA[
        (function() {
            window.onload = function(){
                html2canvas(document.body, {"logging": true, //Enable log (use Web Console for get Errors and Warnings)"proxy":"html2canvasproxy.php","onrendered": function(canvas) {
                        var img = new Image();
                        img.onload = function() {
                            img.onload = null;
                            document.body.appendChild(img);
                        };
                        img.onerror = function() {
                            img.onerror = null;
                            if(window.console.log) {
                                window.console.log("Not loaded image from canvas.toDataURL");
                            } else {
                                alert("Not loaded image from canvas.toDataURL");
                            }
                        };
                        img.src = canvas.toDataURL("image/png");
                    }
                });
            };
        })();
        //]]></script></head><body><p><img alt="google maps static" src="http://maps.googleapis.com/maps/api/staticmap?center=40.714728,-73.998672&zoom=12&size=800x600&maptype=roadmap&sensor=false"></p></body></html>

这个方法只能用在你自己的服务器里,如果是对别人的网页截图,还是不行。

试验的过程中还发现用html2canvas截屏出来的图像有时会出现文字重叠的现象。我估计是因为html2canvas在解析页面内容、处理css时不是很完美的原因。

最后,我在火狐浏览器的官方网站上找到了 drawWindow()这个方法,这个方法和上面提到html2canvas不同之处在于,它不分析页面元素,它只针对区域,也就是说,它接受的参数是四个数字标志的区域,不论这个区域中什么地方,有没有页面内容。

void drawWindow(
  in nsIDOMWindow window,
  in float x, 
  in float y,
  in float w,
  in float h,
  in DOMString bgColor,
  in unsigned long flags [optional]
);

这个原生的JavaScript方法看起来非常的完美,正是我需要的,但这个方法不能使用在普通网页中,因为火狐官方发现这个方法会引起有 安全漏洞,在这个bug修复之前,只有具有“Chrome privileges”的代码才能使用这个 drawWindow()函数。

虽然有很大的限制,但周折一下还是可以用的,在我开发的火狐addon插件中,main.js就是具有“Chrome privileges”的代码。我在网上发现了一段火狐插件SDK里自带 代码样例

var window = require('window/utils').getMostRecentBrowserWindow();
var tab = require('tabs/utils').getActiveTab(window);
var thumbnail = window.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
thumbnail.mozOpaque = true;
window = tab.linkedBrowser.contentWindow;
thumbnail.width = Math.ceil(window.screen.availWidth / 5.75);
var aspectRatio = 0.5625; // 16:9
thumbnail.height = Math.round(thumbnail.width * aspectRatio);
var ctx = thumbnail.getContext("2d");
var snippetWidth = window.innerWidth * .6;
var scale = thumbnail.width / snippetWidth;
ctx.scale(scale, scale);
ctx.drawWindow(window, window.scrollX, window.scrollY, snippetWidth, snippetWidth * aspectRatio, "rgb(255,255,255)");
// thumbnail now represents a thumbnail of the tab

这段代码写的非常清楚,只需要依据它做稍微的修改就能适应自己的需求。

我是第一次接触火狐插件开发,是边学习,边研究,边开发。所以开发速度很慢,这个小小的插件用了整整一周才基本上达到能用的程度。你可以在 @程序师视野微博里看到用它上传的图片效果还是不错的。

先能用,然后使用的过程中慢慢做改进,这是我的软件开发理念。

希望和对火狐插件有兴趣的朋友一起探讨、一起学习。

 

页面重绘和回流以及优化

$
0
0

标签:   回流   重绘

在讨论页面重绘、回流之前。需要对页面的呈现流程有些了解,页面是怎么把html结合css等显示到浏览器上的,下面的流程图显示了浏览器对页面的呈现的处理流程。可能不同的浏览器略微会有些不同。但基本上都是类似的。

8_1

1.  浏览器把获取到的HTML代码解析成1个DOM树,HTML中的每个tag都是DOM树中的1个节点,根节点就是我们常用的document对象。DOM树里包含了所有HTML标签,包括display:none隐藏,还有用JS动态添加的元素等。

2. 浏览器把所有样式(用户定义的CSS和用户代理)解析成样式结构体,在解析的过程中会去掉浏览器不能识别的样式,比如IE会去掉-moz开头的样式,而FF会去掉_开头的样式。

3、DOM Tree 和样式结构体组合后构建render tree, render tree类似于DOM tree,但区别很大,render tree能识别样式,render tree中每个NODE都有自己的style,而且 render tree不包含隐藏的节点 (比如display:none的节点,还有head节点),因为这些节点不会用于呈现,而且不会影响呈现的,所以就不会包含到 render tree中。注意 visibility:hidden隐藏的元素还是会包含到 render tree中的,因为visibility:hidden 会影响布局(layout),会占有空间。根据CSS2的标准,render tree中的每个节点都称为Box (Box dimensions),理解页面元素为一个具有填充、边距、边框和位置的盒子。

4. 一旦render tree构建完毕后,浏览器就可以根据render tree来绘制页面了。

回流与重绘

1. 当render tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候。在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为重绘。

2. 当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color。则就叫称为重绘。

注意:回流必将引起重绘,而重绘不一定会引起回流。


回流何时发生:

当页面布局和几何属性改变时就需要回流。下述情况会发生浏览器回流:

1、添加或者删除可见的DOM元素;

2、元素位置改变;

3、元素尺寸改变——边距、填充、边框、宽度和高度

4、内容改变——比如文本改变或者图片大小改变而引起的计算值宽度和高度改变;

5、页面渲染初始化;

6、浏览器窗口尺寸改变——resize事件发生时;


让我们看看下面的代码是如何影响回流和重绘的:

  1. var s = document.body.style;

  2. s.padding ="2px";// 回流+重绘

  3. s.border ="1px solid red";// 再一次 回流+重绘

  4. s.color ="blue";// 再一次重绘

  5. s.backgroundColor ="#ccc";// 再一次 重绘

  6. s.fontSize ="14px";// 再一次 回流+重绘

  7. // 添加node,再一次 回流+重绘

  8. document.body.appendChild(document.createTextNode('abc!'));

说到这里大家都知道回流比重绘的代价要更高,回流的花销跟render tree有多少节点需要重新构建有关系,假设你直接操作body,比如在body最前面插入1个元素,会导致整个render tree回流,这样代价当然会比较高,但如果是指body后面插入1个元素,则不会影响前面元素的回流。


聪明的浏览器

从上个实例代码中可以看到几行简单的JS代码就引起了6次左右的回流、重绘。而且我们也知道回流的花销也不小,如果每句JS操作都去回流重绘的话,浏览器可能就会受不了。所以很多浏览器都会优化这些操作,浏览器会维护1个队列,把所有会引起回流、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会flush队列,进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。

虽然有了浏览器的优化,但有时候我们写的一些代码可能会强制浏览器提前flush队列,这样浏览器的优化可能就起不到作用了。当你请求向浏览器请求一些 style信息的时候,就会让浏览器flush队列,比如:

1. offsetTop, offsetLeft, offsetWidth, offsetHeight

2. scrollTop/Left/Width/Height

3. clientTop/Left/Width/Height

4. width,height

5. 请求了getComputedStyle(), 或者 IE的 currentStyle

当你请求上面的一些属性的时候,浏览器为了给你最精确的值,需要flush队列,因为队列中可能会有影响到这些值的操作。即使你获取元素的布局和样式信息跟最近发生或改变的布局信息无关,浏览器都会强行刷新渲染队列。

如何减少回流、重绘

减少回流、重绘其实就是需要减少对render tree的操作(合并多次多DOM和样式的修改),并减少对一些style信息的请求,尽量利用好浏览器的优化策略。具体方法有:

1. 直接改变className,如果动态改变样式,则使用cssText(考虑没有优化的浏览器)

  1. // 不好的写法

  2. var left =1;

  3. var top =1;

  4. el.style.left = left +"px";

  5. el.style.top = top +"px";// 比较好的写法

  6. el.className +=" className1";

  7. // 比较好的写法

  8. el.style.cssText +=";

  9. left:" + left + "px;

  10. top:" + top + "px;";

2. 让要操作的元素进行”离线处理”,处理完后一起更新

a) 使用DocumentFragment进行缓存操作,引发一次回流和重绘;
b) 使用display:none技术,只引发两次回流和重绘;
c) 使用cloneNode(true or false) 和 replaceChild 技术,引发一次回流和重绘;


3.不要经常访问会引起浏览器flush队列的属性,如果你确实要访问,利用缓存

  1. // 别这样写,大哥

  2. for(循环){

  3. el.style.left = el.offsetLeft +5+"px";

  4. el.style.top = el.offsetTop +5+"px";

  5. }

  6. // 这样写好点

  7. var left = el.offsetLeft,

  8. top = el.offsetTop,

  9. s = el.style;

  10. for(循环){

  11. left +=10;

  12. top +=10;

  13. s.left = left +"px";

  14. s.top = top +"px";

  15. }

4. 让元素脱离动画流,减少回流的Render Tree的规模

  1. $("#block1").animate({left:50});

  2. $("#block2").animate({marginLeft:50});

实例测试

最后用2个工具对上面的理论进行一些测试,分别是:dynaTrace(测试ie),Speed Tracer(测试Chrome)。

第一个测试代码不改变元素的规则,大小,位置。只改变颜色,所以不存在回流,仅测试重绘,代码如下:

  1. <body>

  2. &l

    您可能还对下面的文章感兴趣:

    1. 浏览器的重绘(repaints)与重排(reflows) [2014-08-15 13:52:56]
    2. 浏览器的重绘[repaints]与重排[reflows] [2012-11-13 13:46:27]

前端模块管理器简介

$
0
0

模块化结构已经成为网站开发的主流。

制作网站的主要工作,不再是自己编写各种功能,而是如何将各种不同的模块组合在一起。

模块化结构

浏览器本身并不提供模块管理的机制,为了调用各个模块,有时不得不在网页中,加入一大堆script标签。这样就使得网页体积臃肿,难以维护,还产生大量的HTTP请求,拖慢显示速度,影响用户体验。

为了解决这个问题,前端的模块管理器(package management)应运而生。它可以轻松管理各种JavaScript脚本的依赖关系,自动加载各个模块,使得网页结构清晰合理。不夸张地说,将来 所有的前端JavaScript项目,应该都会采用这种方式开发。

最早也是最有名的前端模块管理器,非 RequireJS莫属。它采用 AMD格式,异步加载各种模块。具体的用法,可以参考我写的 教程。Require.js的问题在于各种参数设置过于繁琐,不容易学习,很难完全掌握。而且,实际应用中,往往还需要在服务器端,将所有模块合并后,再统一加载,这多出了很多工作量。

RequireJS

今天,我介绍另外四种前端模块管理器: BowerBrowserifyComponentDuo。它们各自都有鲜明的特点,很好地弥补了Require.js的缺陷,是前端开发的利器。

需要说明的是,这篇文章并不是这四种模块管理器的教程。我只是想用最简单的例子,说明它们是干什么用的,使得读者有一个大致的印象,知道某一种工作有特定的工具可以完成。详细的用法,还需要参考它们各自的文档。

Bower

Bower

Bower的主要作用是,为模块的安装、升级和删除,提供一种统一的、可维护的管理模式。

首先,安装Bower。


  $ npm install -g bower

然后,使用bower install命令安装各种模块。下面是一些例子。


  # 模块的名称
  $ bower install jquery
  # github用户名/项目名
  $ bower install jquery/jquery
  # git代码仓库地址
  $ bower install git://github.com/user/package.git
  # 模块网址
  $ bower install http://example.com/script.js

所谓"安装",就是将该模块(以及其依赖的模块)下载到当前目录的bower_components子目录中。下载后,就可以直接插入网页。


  <script src="/bower_componets/jquery/dist/jquery.min.js">

bower update命令用于更新模块。


  $ bower update jquery

如果不给出模块的名称,则更新所有模块。

bower uninstall命令用于卸载模块。


  $ bower uninstall jquery

注意,默认情况下,会连所依赖的模块一起卸载。比如,如果卸载jquery-ui,会连jquery一起卸载,除非还有别的模块依赖jquery。

Browserify

Browserify

Browserify本身不是模块管理器,只是让服务器端的CommonJS格式的模块可以运行在浏览器端。这意味着通过它,我们可以使用Node.js的npm模块管理器。所以,实际上,它等于间接为浏览器提供了npm的功能。

首先,安装Browserify。


  $ npm install -g browserify

然后,编写一个服务器端脚本。


  var uniq = require('uniq');
  var nums = [ 5, 2, 1, 3, 2, 5, 4, 2, 0, 1 ];
  console.log(uniq(nums));

上面代码中uniq模块是CommonJS格式,无法在浏览器中运行。这时,Browserify就登场了,将上面代码编译为浏览器脚本。


  $ browserify robot.js > bundle.js

生成的bundle.js可以直接插入网页。


  <script src="bundle.js"></script>

Browserify编译的时候,会将脚本所依赖的模块一起编译进去。这意味着,它可以将多个模块合并成一个文件。

Component

Component

Component是Express框架的作者TJ Holowaychuk开发的模块管理器。它的基本思想,是将网页所需要的各种资源(脚本、样式表、图片、字体等)编译后,放到同一个目录中(默认是build目录)。

首先,安装Component。


  $ npm install -g component@1.0.0-rc5

上面代码之所以需要指定Component的版本,是因为1.0版还没有正式发布。

然后,在项目根目录下,新建一个index.html。


  <!DOCTYPE html>
  <html>
    <head>
      <title>Getting Started with Component</title>
      <link rel="stylesheet" href="build/build.css">
    </head>
    <body>
      <h1>Getting Started with Component</h1>
      <p class="blink">Woo!</p>     <script src="build/build.js"></script>
    </body>
  </html>

上面代码中的build.css和build.js,就是Component所要生成的目标文件。

接着,在项目根目录下,新建一个component.json文件,作为项目的配置文件。


  {
    "name": "getting-started-with-component",
    "dependencies": {
      "necolas/normalize.css": "^3.0.0"
    },
    "scripts": ["index.js"],
    "styles": ["index.css"]
  }

上面代码中,指定JavaScript脚本和样式表的原始文件是index.js和index.css两个文件,并且样式表依赖normalize模块(不低于3.0.0版本,但不高于4.0版本)。这里需要注意,Component模块的格式是"github用户名/项目名"。

最后,运行component build命令编译文件。


  $ component build
     installed : necolas/normalize.css@3.0.1 in 267ms
         build : resolved in 1221ms
         build : files in 12ms
         build : build/build.js in 76ms - 1kb
         build : build/build.css in 80ms - 7kb

在编译的时候,Component自动使用 autoprefixer为CSS属性加上浏览器前缀。

目前,Component似乎处于停止开发的状态,代码仓库已经将近半年没有变动过了,官方也推荐优先使用接下来介绍的Duo。

Duo

Duo

Duo是在Component的基础上开发的,语法和配置文件基本通用,并且借鉴了Browserify和Go语言的一些特点,相当地强大和好用。

首先,安装Duo。


  $ npm install -g duo

然后,编写一个本地文件index.js。


  var uid = require('matthewmueller/uid');
  var fmt = require('yields/fmt');
  
  var msg = fmt('Your unique ID is %s!', uid());
  window.alert(msg);

上面代码加载了uid和fmt两个模块,采用Component的"github用户名/项目名"格式。

接着,编译最终的脚本文件。


  $ duo index.js > build.js

编译后的文件可以直接插入网页。


  <script src="build.js"></script>

Duo不仅可以编译JavaScript,还可以编译CSS。


  @import 'necolas/normalize.css';
  @import './layout/layout.css';
  
  body {
    color: teal;
    background: url('./background-image.jpg');
  }

编译时,Duo自动将normalize.css和layout.css,与当前样式表合并成同一个文件。


  $ duo index.css > build.css

编译后,插入网页即可。


  <link rel="stylesheet" href="build.css">

(完)

文档信息

如何捕获和分析 JavaScript Error

$
0
0

标签:   Error

前端工程师都知道 JavaScript 有基本的异常处理能力。我们可以 throw new Error(),浏览器也会在我们调用 API 出错时抛出异常。但估计绝大多数前端工程师都没考虑过收集这些异常信息。反正只要 JavaScript 出错后刷新不复现,那用户就可以通过刷新解决问题,浏览器不会崩溃,当没有发生过好了。这种假设在 Single Page App 流行之前还是成立的。现在的 Single Page App 运行一段时间后状态复杂无比,用户可能进行了若干输入操作才来到这里的,说刷新就刷新啊?之前的操作岂不要完全重做?所以我们还是有必要捕获和分析这些异常信息的,然后我们就可以修改代码避免影响用户体验。

捕获异常的方式

我们自己写的 throw new Error()想要捕获当然可以捕获,因为我们很清楚 throw写在哪里了。但是调用浏览器 API 时发生的异常就不一定那么容易捕获了,有些 API 在标准里就写着会抛出异常,有些 API 只有个别浏览器因为实现差异或者有缺陷而抛出异常。对于前者我们还能通过 try-catch捕获,对于后者我们必须监听全局的异常然后捕获。

try-catch

如果有些浏览器 API 是已知会抛出异常的,那我们就需要把调用放到 try-catch里面,避免因为出错而导致整个程序进入非法状态。例如说 window.localStorage就是这样的一个 API,在写入数据超过容量限制后就会 抛出异常,在 Safari 的隐私浏览模式下也会如此。

  1. try{

  2.  localStorage.setItem('date',Date.now());

  3. }catch(error){

  4.  reportError(error);

  5. }

另一个常见的 try-catch适用场景是回调。因为回调函数的代码是我们不可控的,代码质量如何,会不会调用其它会抛出异常的 API,我们一概不知道。为了不要因为回调出错而导致调用回调后的其它代码无法执行,所以把调用回到放到 try-catch里面是必须的。

  1. listeners.forEach(function(listener){

  2. try{

  3.    listener();

  4. }catch(error){

  5.    reportError(error);

  6. }

  7. });

window.onerror

对于 try-catch覆盖不到的地方,如果出现异常就只能通过 window.onerror来捕获了。

  1. window.onerror =

  2. function(errorMessage, scriptURI, lineNumber){

  3.    reportError({

  4.      message: errorMessage,

  5.      script: scriptURI,

  6.      line: lineNumber

  7. });

  8. }

注意不要耍小聪明使用 window.addEventListenerwindow.attachEvent的形式去监听 window.onerror。很多浏览器只实现了 window.onerror,或者是只有 window.onerror的实现是标准的。考虑到标准草案定义的也是 window.onerror,我们使用 window.onerror就好了。

属性丢失

假设我们有一个 reportError函数用来收集捕获到的异常,然后批量发送到服务器端存储以便查询分析,那么我们会想要收集哪些信息呢?比较有用的信息包括:错误类型( name)、错误消息( message)、脚本文件地址( script)、行号( line)、列号( column)、堆栈跟踪( stack)。如果一个异常是通过 try-catch捕获到的,这些信息都在 Error对象上(主流浏览器都支持),所以 reportError也能收集到这些信息。但如果是通过 window.onerror捕获到的,我们都知道这个事件函数只有 3 个参数,所以这 3 个参数以外的信息就丢失了。

序列化消息

如果 Error对象是我们自己创建的话,那么 error.message就是由我们控制的。基本上我们把什么放进 error.message里面, window.onerror的第一个参数( message)就会是什么。(浏览器其实会略作修改,例如加上 'Uncaught Error: '前缀。)因此我们可以把我们关注的属性序列化(例如 JSON.Stringify)后存放到 error.message里面,然后在 window.onerror读取出来反序列化就可以了。当然,这仅限于我们自己创建的 Error对象。

第五个参数

浏览器厂商也知道大家在使用 window.onerror时受到的限制,所以开始往 window.onerror上面添加新的参数。考虑到只有行号没有列号好像不是很对称的样子,IE 首先把列号加上了,放在第四个参数。然而大家更关心的是能否拿到完整的堆栈,于是 Firefox 说不如把堆栈放在第五个参数吧。但 Chrome 说那还不如把整个 Error对象放在第五个参数,大家想读取什么属性都可以了,包括自定义属性。结果由于 Chrome 动作比较快,在 Chrome 30 实现了新的 window.onerror签名,导致 标准草案也就跟着这样写了。

  1. window.onerror =function(

  2.  errorMessage,

  3.  scriptURI,

  4.  lineNumber,

  5.  columnNumber,

  6.  error

  7. ){

  8. if(error){

  9.    reportError(error);

  10. }else{

  11.    reportError({

  12.      message: errorMessage,

  13.      script: scriptURI,

  14.      line: lineNumber,

  15.      column: columnNumber

  16. });

  17. }

  18. }

属性正规化

我们之前讨论到的 Error对象属性,其名称都是基于 Chrome 命名方式的,然而不同浏览器对 Error对象属性的命名方式各不相同,例如脚本文件地址在 Chrome 叫做

如何用JavaScript获取图片的真实尺寸大小

$
0
0

网页页面上的图片尺寸似乎都千篇一律。我们最常见到的带有多图的文章页面中,图的大小通常是和页面的宽度一致,这样看起来,页面就是一个直筒形,这样的布局看多了就会觉得很单调。之所以形成这样的局面,我想很大程度上是因为老式浏览器的限制。但随着现代浏览器(火狐/谷歌/IE11)的普及,浏览器对页面设计的限制越来越少,Web程序员的想象能力能够得到极大的发挥。

比如, 冷知识:你知道每个视窗都有的 [x] 是怎么来的吗?这篇文章中,很多图片超出了文本宽度的限制,给人一种参差错落的感觉,同时,让大图片以其真实的尺寸展示,给人以更震撼的感觉。

但从技术上,我们可以轻松的用文本的最大宽度限制图片,让它们都保持一个宽度,而不按文本的宽度时,我们就需要每个图片的自己的尺寸。我们可以在服务端编辑时声明图片的原始尺寸。而一种更灵活的方式是通过在页面上放一段js来动态的获取图片的原始大小尺寸,动态改变图片的显示大小。这样即能兼容老的也文本最大宽度的方式,还可以在需要的时候让图片呈现出其原始的大小。

如何用JavaScript在浏览器端获取图片的原始尺寸大小?

var img = $("#img_id"); // Get my img elem
var pic_real_width, pic_real_height;
$("<img/>") // Make in memory copy of image to avoid css issues
    .attr("src", $(img).attr("src"))
    .load(function() {
        pic_real_width = this.width;   // Note: $(this).width() will not
        pic_real_height = this.height; // work for in memory images.
    });

Webkit浏览器(谷歌浏览器等)是在图片的loaded事件之后才能获取高度和宽度值。所以,你不能使用timeout函数延时等待,最好的方法是使用图片的onload事件。

为了避免CSS对图片大小尺寸的影响,上面的代码将图片拷贝到内存中进行计算。

如果你的页面是老式页面,你可以按需把这段代码嵌入页面底部,它不需要你修改原有页面。

参考 stackoverflow

JavaScript 运行机制详解:再谈Event Loop

$
0
0

一年前,我写了一篇 《什么是 Event Loop?》,谈了我对Event Loop的理解。

上个月,我偶然看到了Philip Roberts的演讲 《Help, I'm stuck in an event-loop》。这才尴尬地发现,自己的理解是错的。我决定重写这个题目,详细、完整、正确地描述JavaScript引擎的内部运行机制。下面就是我的重写。

进入正文之前,插播一条消息。我的新书 《ECMAScript 6入门》出版了( 版权页内页1内页2),铜版纸全彩印刷,非常精美,还附有索引(当然价格也比同类书籍略贵一点点)。预览和购买点击 这里

cover

一、为什么JavaScript是单线程?

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

二、任务队列

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。

JavaScript语言的设计者意识到,这时CPU完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

于是,JavaScript就有了两种执行方式:一种是CPU按顺序执行,前一个任务结束,再执行下一个任务,这叫做同步执行;另一种是CPU跳过等待时间长的任务,先处理后面的任务,这叫做异步执行。程序员自主选择,采用哪种执行方式。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

(1)所有任务都在主线程上执行,形成一个 执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。系统把异步任务放到"任务队列"之中,然后继续执行后续的任务。

(3)一旦"执行栈"中的所有任务执行完毕,系统就会检查"任务队列"。如果这个时候,异步任务已经结束了等待状态,就会从"任务队列"进入执行栈,恢复执行。

(4)主线程不断重复上面的第三步。

下图就是主线程和任务队列的示意图。

任务队列

只要主线程空了,就会去检查"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。

三、事件和回调函数

"任务队列"实质上是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程检查"任务队列",就是检查里面有哪些事件。

"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。

所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当异步任务从"任务队列"回到执行栈,回调函数就会执行。

四、Event Loop

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

为了更好地理解Event Loop,请看下图(转引自Philip Roberts的演讲 《Help, I'm stuck in an event-loop》)。

Event Loop

上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去检查"任务队列",看看哪些事件已经完成了,并执行对应的回调函数。

执行栈中的代码,总是在异步任务之前执行。请看下面这个例子。


    var req = new XMLHttpRequest();
    req.open('GET', url);    
    req.onload = function (){};    
    req.onerror = function (){};    
    req.send();

上面代码中的req.send方法是Ajax操作向服务器发送数据,它是一个异步任务,意味着只有当前脚本的所有代码执行完,系统才会去"任务队列"检查是否有返回结果。所以,它与下面的写法等价。


    var req = new XMLHttpRequest();
    req.open('GET', url);    
    req.send();
    req.onload = function (){};    
    req.onerror = function (){};   

也就是说,指定回调函数的部分(onload和onerror),在send()方法的前面或后面无关紧要,因为它们属于执行栈的一部分,系统总是执行完它们,才会去检查"任务队列"。

五、定时器

除了插入异步任务,"任务队列"还有一个作用,就是可以插入定时事件,即指定某些代码在多少时间之后执行。这叫做"定时器"(timer)功能,即定时执行代码。

定时器功能主要由setTimeout()和setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。以下主要讨论setTimeout()。

setTimeout()接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。


console.log(1);

setTimeout(function(){
    console.log(2);
},1000);

console.log(3);

上面代码的执行结果是1,3,2,因为setTimeout()将第二行推迟到1000毫秒之后执行。

如果将setTimeout()的第二个参数设为0,就表示当前代码执行完,立即执行(0毫秒间隔)指定的回调函数。


setTimeout(function(){
    console.log(1);
}, 0);

console.log(2);

上面代码的执行结果总是2,1,因为只有在执行完第二行以后,系统才会去执行"任务队列"中的回调函数。

有些浏览器规定了setTimeout()的第二个参数的最小值,比如Firefox规定不得低于4毫秒,如果低于这个值,就会自动调整。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimFrame()的效果要好于setTimeout()。

需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。

六、Node.js的Event Loop

Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。

请看下面的示意图(作者 @BusyRich)。

Node.js

根据上图,Node.js的运行机制如下。

(1)V8引擎解析JavaScript脚本。

(2)解析后的代码,调用Node API。

(3) libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。

(4)V8引擎再将结果返回给用户。

Node.js有一个 process.nextTick()方法,可以将指定事件推迟到Event Loop的下一次执行,也就是当前的执行栈清空之后立即执行。


function foo() {
    console.error(1);
}

process.nextTick(foo);
console.log(2);
// 2
// 1

process.nextTick(foo)的作用,与setTimeout(foo, 0)很相似,但是执行效率 高得多

(完)

文档信息

Torrent Tornado:浏览器内 BT 下载

$
0
0

Torrent Tornado 是一款完全使用 JavaScript 实现的附加组件,可以为 Firefox 浏览器增加 BT 下载功能。

torrent-tornado

该附加组件的亮点有:

  • 体积小巧(不到 100K),完全使用 JavaScript 实现,跨平台且无本地二进制依赖。

  • 支持和磁力链接及种子文件关联。

  • 支持拖放式启动下载。

注意当前 1.0 版本仅支持下载,不支持上传。

Firefox 扩展下载

分类: BitTorrent Client, Firefox Extension | 永久链接 |Email 给好友 | 无评论 |捐助本站


浅谈事件冒泡与事件捕获

$
0
0

事件冒泡与事件捕获

事件冒泡和事件捕获分别由微软和网景公司提出,这两个概念都是为了解决页面中 事件流(事件发生顺序)的问题。

<div id="outer"><p id="inner">Click me!</p></div>

上面的代码当中一个div元素当中有一个p子元素,如果两个元素都有一个click的处理函数,那么我们怎么才能知道哪一个函数会首先被触发呢?

为了解决这个问题微软和网景提出了两种几乎完全相反的概念。

事件冒泡

微软提出了名为 事件冒泡(event bubbling)的事件流。事件冒泡可以形象地比喻为把一颗石头投入水中,泡泡会一直从水底冒出水面。也就是说,事件会从最内层的元素开始发生,一直向上传播,直到document对象。

因此上面的例子在事件冒泡的概念下发生click事件的顺序应该是 p -> div -> body -> html -> document

事件捕获

网景提出另一种事件流名为 事件捕获(event capturing)。与事件冒泡相反,事件会从最外层开始发生,直到最具体的元素。

上面的例子在事件捕获的概念下发生click事件的顺序应该是 document -> html -> body -> div -> p

addEventListener的第三个参数

“DOM2级事件”中规定的事件流同时支持了事件捕获阶段和事件冒泡阶段,而作为开发者,我们可以选择事件处理函数在哪一个阶段被调用。

addEventListener方法用来为一个特定的元素绑定一个事件处理函数,是JavaScript中的常用方法。addEventListener有三个参数:

element.addEventListener(event, function, useCapture)

第一个参数是需要绑定的事件,第二个参数是触发事件后要执行的函数。而第三个参数默认值是false,表示在事件冒泡的阶段调用事件处理函数,如果参数为true,则表示在事件捕获阶段调用处理函数。请看 例子

事件代理

在实际的开发当中,利用事件流的特性,我们可以使用一种叫做事件代理的方法。

<ul id="color-list"><li>red</li><li>yellow</li><li>blue</li><li>green</li><li>black</li><li>white</li></ul>

如果点击页面中的li元素,然后输出li当中的颜色,我们通常会这样写:

(function(){
    var color_list = document.getElementById('color-list');
    var colors = color_list.getElementsByTagName('li');
    for(var i=0;i<colors.length;i++){                          colors[i].addEventListener('click',showColor,false);
    };
    function showColor(e){
        var x = e.target;
        alert("The color is " + x.innerHTML);
    };
})();

利用事件流的特性,我们只绑定一个事件处理函数也可以完成:

(function(){
    var color_list = document.getElementById('color-list');
    color_list.addEventListener('click',showColor,false);
    function showColor(e){
        var x = e.target;
        if(x.nodeName.toLowerCase() === 'li'){
            alert('The color is ' + x.innerHTML);
        }
    }
})();

使用事件代理的好处不仅在于将多个事件处理函数减为一个,而且对于不同的元素可以有不同的处理方法。假如上述列表元素当中添加了其他的元素(如:a、span等),我们不必再一次循环给每一个元素绑定事件,直接修改事件代理的事件处理函数即可。

冒泡还是捕获?

对于事件代理来说,在事件捕获或者事件冒泡阶段处理并没有明显的优劣之分,但是由于事件冒泡的事件流模型被所有主流的浏览器兼容,从兼容性角度来说还是建议大家使用事件冒泡模型。

IE浏览器兼容

IE浏览器对addEventListener兼容性并不算太好,只有IE9以上可以使用。

addEventListener兼容性

要兼容旧版本的IE浏览器,可以使用IE的attachEvent函数

object.attachEvent(event, function)

两个参数与addEventListener相似,分别是事件和处理函数,默认是事件冒泡阶段调用处理函数,要注意的是,写事件名时候要加上"on"前缀("onload"、"onclick"等)。

感谢您的阅读,有不足之处请为我指出。

参考

  1. HTML DOM addEventListener() Method -- w3schools
  2. JavaScript高级程序设计(第3版)
  3. attachEvent method -- MSDN
  4. What is event bubbling and capturing -- Stack Overflow

文章同步在我的博客,本文链接: http://acwong.org/2014/10/28/bubbling-and-capturing/

探真无阻塞加载javascript脚本技术,我们会发现很多意想不到的秘密

$
0
0

下面的图片是我使用firefox和chrome浏览百度首页时候记录的http请求

下面是firefox:

下面是chrome:

在浏览百度首页前我都将浏览器的缓存全部清理掉,让这个场景最接近第一次访问百度首页的情景。

在firefox的请求瀑布图里有个表现非常之明显:就是javascript文件下载完毕后,有一段时间是没有网络请求被处理的,这段时间过后http请求才会接着执行,这段空闲时间就是所谓的http请求被阻塞。

浏览器里的http请求被阻塞一般都是由javascript所引起,具体原因是javascript下载完毕之后会立即执行,而javascript执行时候会阻塞浏览器的其他行为,例如阻塞其他javascript的执行以及其他的http请求的执行。这样会导致页面加载变慢,如果这个变慢很明显,此时用户操作网页会发现页面没有反应会反应很慢,慢是网站用户体验的梦魇。

我目前开发的一些系统,在开发环境里经常碰到javascript阻塞页面加载的问题,主要原因是我们网站很多静态资源和脚本都被独立抽取在了一台单独的静态资源服务器上,而本地的开发环境模拟的静态资源服务环境常常很不稳定(经常宕机),有时一些新建的脚本没有及时更新到开发环境上,因此某些js脚本文件无法正确获取,这些问题导致页面加载时候这些js脚本就会阻塞页面的加载,此时浏览器会反复尝试请求这些js文件,直到请求超时才会认定该脚本的url无效,如果中途你无法忍受这种等待,强制关闭浏览器的请求,会发现在浏览器控制台里很多脚本都无法找到,这样你就无法在控制台里设置js代码断点调试js,如果等待js加载完毕,时间又会被浪费,无奈之下只要找到那些无效的url将其注释掉,哎,结果好几次都将有注释的错误代码提交到了svn服务器上,这些事情真是很恼人。

不管那种浏览器,也不管是新版本还是老版本的浏览器,它们都秉持浏览器的单线程特性,这个特性似乎是一个很难撼动的准则,当我们在浏览器的地址栏里输入一个url地址,访问一个新页面时候,页面展示的快慢就是由一个单线程所控制,这个线程叫做UI线程,UI线程会根据页面里资源(资源是指css文件,图片等等)书写的先后顺序来加载资源,加载资源也就是使用http请求获取资源,像css外部文件,html文件以及图片等资源http请求处理完毕也就意味着资源加载结束,但是像外部的javascript文件则不同,它的加载过程被分为两步,第一步和加载css文件和图片一样,就是执行一个http请求下载外部的js文件,但是javascript完成http操作后并不意味操作完毕,UI线程接着会执行它,如果你开发的页面里js代码执行时间过长,那么用户就会明显感觉到页面的延迟。为什么浏览器不能把javascript代码的加载过程拆分为下载和执行两个并行的过程,这样就可以充分利用时间完成http请求,这样不是就能提升页面的加载效率了吗?这个问题的答案当然是否定的,javascript是一个编程语言,js代码是有智力的,它除了可以完成逻辑性的工作,还可以通过操作页面元素来改变页面UI效果,如果我们忽略javascript对UI的影响,让它延迟执行,结果会造成页面展示的混乱。那么他会产生什么样的混乱呢?这个混乱的描述如下:

最简单最好理解和最好掌握的思路是线性思路,对于浏览器UI显示要按线性思路理解即放在页面前部的资源会优先加载和执行,浏览器还会认为前一步的内容都可能会是后一步页面展示前提,如果浏览器擅自停止中间某个代码的执行,很有可能页面最终呈现的效果和设计者设计的不同,这样我们就无法开发出正确的页面。而且按线性思路当你碰到页面UI效果出问题时候,你很容易定位问题所在,如果我们将js代码的加载和执行分隔开来,这就好比把线性思路变成了树状结构,那么你掌握页面加载的思路和解决UI加载问题时候就会变得更加困难,到时很多人都会抓狂和思路混乱,所以我在这里说混乱。

综上所述,js脚本下载和执行是一个完整的操作,是绝对不能被割裂的。

浏览器为了提升用户体验,加快UI线程的执行是一个无法回避的问题,看来拆分js的下载和执行是不可行的,如是乎浏览器换了种方式,这个方式也就是在同一个时间能下载多个资源,我们再看上图,在同一个域名下,firefox可以同时下载两种图片,chrome可以同时下载4个静态资源,不过这是针对图片和css文件,对于js文件似乎还是一个接着一个的下载,下载一个执行一个,不过在ie8以上版本,js可以和图片一样并行加载,ie这样做就提升了js文件下载的效率,不过到了js执行时候还是要严格按照顺序执行。

多个http连接并行下载资源就好比多个线程共同完成某个任务,如果并行http连接更多,那么能有更多http资源同时被下载,但是浏览器提供并行执行的http连接实在太少了,例如上面firefox才两个,chrome也只有4个,那如何突破浏览器的连接个数的限制了?方法很简单就是将常用的,稳定的静态资源统一放在一个静态资源服务器上,由统一的域名对外提供,这个域名要和主体请求的域名不一样,原理是因为浏览器只通过域名来限制连接的个数,如果一个页面里有两个不同的域的,那么并行的http请求个数也会变成两倍。这个做法有点讨浏览器的巧,是程序员发现浏览器的特点而总结出的技术,它类似一个hack技术,而hack技术不会是标准技术,所以它肯定有它的瓶颈区,所以这样的技术都是会有个度的,浏览器限制请求个数绝对不是无缘无故的,我们看百度页面并行下载图片的http协议的版本都是1.1,http1.1特点就是长连接,长连接的好处是在页面和服务端频繁交互时候效率很好,但是浏览器的页面操作并不是总是频繁的请求服务器,而为了加载静态资源而创建很多长连接,服务器会不得不维护大量无用的长连接,给服务器的压力是可想而知的。相比之下http1.0协议,它不使用长连接,而是短连接,因此早期版本的浏览器会给http1.0协议开启的连接数要高于http1.1连接数,因此有些网站将静态资源服务器提供的http协议版本降低到1.0,这样可以有效的提升浏览器的并发连接数,这个做法会给网页性能带来意想不到的提升,不过现代的浏览器似乎更愿意平等对待两个不同版本的协议了,新版的浏览器有些将两类协议的并发数变成一样了。而对于处于客户端地位的浏览器维护多个链接对于资源消耗是庞大的,而且域名过多也会增加dns解析的开销,所以并发连接开启越多,并不一定真的会达到提升性能的目的,那么多少个域最合适了?雅虎的前端工程师给了一个答案:2个是最佳的,这个数据怎么得来的我就不太清楚了,不过结果很简单很好用,记住就行了。

下面我就要聊聊如何解决js阻塞页面加载的问题了,js之所以会阻塞UI线程的执行,是因为js能控制UI的展示,而页面加载的规则是要顺序执行,所以在碰到js代码时候UI线程会首先执行它,而这点很多程序员不知道或者知道但被忽视,因此导致编写代码时候将用于展示的代码和用于处理逻辑的代码混淆在一起,这样做的后果是使js代码造成的阻塞更加严重,所以雅虎公司的前端团队提出了一个前端优化的规则:将js脚本放置到html文档的末尾,这样就能有效的避免UI的阻塞。但是这个方法太简单了,不利于我们对网站进行更加深入的优化。为了做的深入,我们要需要更进一步分析,首先我们知道js脚本按出现的位置分为两类一类是行内脚本即写在页面里的脚本,一类是js的外部文件,行内脚本的优化比较简单,就是尽量在页面写更少的代码,就算要写代码也主要是控制页面加载的UI显示的代码,没必要的代码就放在外部的js文件里,js外部文件优化就比较复杂,为了精简行内脚本,我们就不得不将大量的js代码放到外部文件里,早先时候我都会尽量将所有外部js文件合并成一个js文件,但是现在我发现一个复杂的外部脚本很有可能会让页面的阻塞情况变得更加的严重,因此外部脚本要根据其功能拆分为展示脚本和逻辑脚本,但是事实上展示代码和逻辑代码很难分离,其实有个更加简单的标准让我们拆分代码:将所有外部代码分为UI初始化代码和其他代码,,UI初始化代码是在页面加载时候执行的代码,我们现在只要判断哪些代码在页面加载时候执行就行了,这个标准就容易多了。

另外,上文我提到过我碰到页面被js阻塞的情况,有时我为了调试js代码会一直等待浏览器判断无效的js加载失败,那么我是怎么判断浏览器已经判断外部js加载无效了?很简单就是查看浏览器忙指示结束,浏览器的忙指示如下图所示:

忙指示(忙指示现象包括:浏览器选项卡的旋转圆圈,鼠标变成漏斗鼠标,浏览器左下的状态栏显示正在加载某某url以及老版的ie显示页面加载进度的现象)标记结束了,就表明页面的加载操作结束了,为了防止js脚本阻塞页面加载,那么我们要做到的就是让那些不会用于页面初始化展示的js代码的加载和执行操作在浏览器忙指示结束后触发,为了做到这一点我们就得知道忙指示结束后会触发什么命令,这个命令就是浏览器的onload事件,因此我们让那些和页面加载无关的js脚本在onload方法里执行,在onload事件里我就会使用dom技术,构建script节点,设置它的src指向需要加载的脚本路径,然后将这个srcipt节点加入到html文档的head里,为了完全确保这个js在忙指示结束后执行,我使用setTimeout方法调用动态加载脚本的方法,进一步确保代码在忙指示结束后执行,实践下来感觉效果的确不错。

理解到这里我本来很高兴,认为自己又理解了一个前端开发的难点,并且有一个好的解决方案,但是等我回味一下发现有点不对头,我经常使用的jQuery定义了ready方法,ready方法会在dom加载完毕后执行,而我自己的方案却是在onload后执行,代码执行远远落后jQuery的ready方法执行时机,这个感觉让我很不舒服,其次,在页面开发里我们会使用很多第三方库,虽然我现在开发尽量做到只用jQuery这一个第三方库,但是其他人则不尽然,他们会使用很多第三方库,很多库有大量UI操作的通用方法,这些方法非常好用,经常使用这些库会导致我们自己写的控制UI的js代码常常会依赖它们,那么拆分UI控制脚本和其他脚本就无从谈起了。总之,现在web前端开发太依赖第三方库,就算一个牛逼的前端团队,拒绝使用第三方库,什么都自己开发,当web应用变复杂后,通用库和业务代码的耦合度都是很难解决的问题,这也会导致我们没法真正将UI展示代码和逻辑代码真正的分离。

我的方案其实满足不了实际生产的需求,不够完美,所以本文到这里没有推导出通用规则,真令人失望,面对上面的新问题那我们该怎么办了?这个问题不是无解的,现行技术就有它的解决方案,那就是无阻塞脚本的加载。无阻塞加载脚本技术的核心就是:加载js脚本时候,被加载的js脚本不会阻塞UI线程的执行和以阻塞方式加载的脚本。

下面是无阻塞加载脚本的技术方案:

XHR Eval

顾名思义,通过XHR读取脚本,通过Eval令脚本生效。

代码如下:

var xhrObj = new XMLHttpRequest();
xhrObj.onreadystatechange = function(){
    if(xhrObj.readyState == 4 && 200 == xhrObj.status){
        eval(xhrObj.responseText);
    }
};

xhrObj.open("GET", "A.js", true);
xhrObj.send("");

由于XMLHttpRequest本身不能跨域,所以该方法不能跨域。

XHR Injection

使用动态创建script元素,来写入脚本,在某些情况下可能比上一种方法要快些。

代码如下:

var xhrObj = new XMLHttpRequest();
xhrObj.onreadystatechange = function(){
    if(xhrObj.readyState == 4){
        var scriptElem = document.createElement("script");
        document.getElementsByTagName("head")[0].appendChild(scriptElem);
        scriptElem.text = xhrObj.responseText;
    }
};
xhrObj.open("GET", "A.js", true);
xhrObj.send("");

Script in Iframe

由于Iframe是开销最高的DOM元素,这种方法还是尽量避免使用,而且这种方法也无法实现跨域。

Script DOM Element

可跨域方案,利用动态插入script元素来让脚本读取、生效。

代码如下:

var scriptElem = document.createElement("script");
scriptElem.src = "http://anydomain.com/A.js";
document.getElementByTagName("head")[0].appendChild(scriptElem);

Script Defer

原生方案。利用defer属性来防止脚本阻塞。

代码如下:

<script defer src="A.js"></script>

不过许多浏览器不支持该属性。

document.write Script Tag

动态写脚本的另一种方案,不过只在IE中是并行下载的。

代码如下:

document.write("<script type='text/javascript' src='A.js'></script>");

script defer和document.write Srcipt Tag不是跨浏览器的方案这里不推荐。
页面嵌套iframe方案我没有详述,原因是我现在很讨厌iframe,iframe是dom元素里开销最大的元素,有它就意味着慢,而且我最近碰到一个生产问题就是iframe引起,原因就是对iframe跨域造成,iframe跨域以后,父窗体和子窗体代码就不能互访了,而且iframe写法的不正确(写的很类似跨站脚本挟持)还会导致浏览器启动默认的安全机制,最终出现用户无法正常使用页面的情况,所以我也不推荐使用iframe。
xhr eval也是我不会去使用的方式,因为它用eval命令,而eval的使用常常会为黑客留下破坏你网站的漏洞。

因此最好的方案就是xhr 注入和script dom element了,这两个方案不存在浏览器兼容问题,而且后者还能跨域,不过跨域的选择也是要谨慎的,跨域脚本也会带来隐形的安全风险,不管怎么说这两个方案使用场景基本上可以包括所有阻塞脚本加载的场景。

注意:无阻塞加载脚本的核心技术就是动态的创建script的dom节点。

无阻塞脚本加载技术还有个好处就是,那些和页面展示无关的脚本无须非要放在onload事件里执行,它随时随地可以运行简直就是完美。

不过无阻塞脚本有个很大的隐患,这个隐患是很多会使用无阻塞脚本技术的程序员都会忽视的问题,这个问题就是无阻塞脚本很容易产生“变量未定义”的问题,这个问题的本质就是无阻塞脚本会破坏js脚本加载顺序的问题,当某个脚本依赖于另一个脚本时候,而另一个脚本又没有加载执行完毕,最后就会产生“变量未定义”的问题,例如jQuery没有提前加载,因此使用$时候提示$变量未定义。
那么我们该如何解决这个问题了,我们的思路就是让那些依赖于无阻塞加载的脚本的js代码在脚本加载完毕后才会执行,我们需要一个办法将无序的脚本加载变得有序,上面我推荐的两种方法都是使用dom技术创建script节点,然后将该节点加入到文档的head头部,对于script节点,在非ie浏览器下有一个onload事件,该事件会在script加载完毕后才会执行,ie浏览器下有onreadystatechange事件,而ie下script的dom节点有一个readystate属性,它的取值如下:
1.uninitialized(未初始化):对象存在尚未初始化;
2.loading(正在加载):对象正在加载数据;
3.loaded(加载完毕):对象数据加载完成
4.interactive(交互):可以操作对象,但是还没有完全加载;
5.complete(完成):对象已经加载完毕。
具体用法如下所示:

scriptNode.onreadystatechange = function(){
    if (scriptNode.readystate == 'complete'){// todo......}
}

这个做法就是为dom加载定义了一个回调函数,当dom加载完毕后回调函数才会执行,这样就解决了代码执行顺序的问题了。

另外还有一个方式就是使用setTimeout,具体使用就是定义一个轮询,判断需要使用的变量是否存在,如果不存在,就继续轮询,如果变量存在则停止轮询,代码模式如下所示:

代码如下:

function lunxun(){
    if ("undefined" == typeof(XXXX)){
        setTimeout(lunxun,300);
    }else{
        ftn();
    }
}
lunxun();

无阻塞脚本的好处就是不会阻塞UI的执行,也不会影响其他同步js代码的执行,不过无阻塞脚本改变了脚本的加载顺序,所以在使用无阻塞脚本时候一定要更加注意脚本之间的依赖关系,保证整个页面的脚本都能正常执行。

在以前的文章里我多次提到了js的模块加载技术,时下流行的模块加载技术有进口货requirejs和国产货seajs,使用这些技术,我们会发现js文件加载都是按模块加载的,也就是说你页面定义了多少个js模块,那么这个页面就有多少个js文件,刚开始使用它们时候我很诧异,按照前端优化原则http请求越少越好,为什么先进的模块技术却会让js文件变得更多了,接着我分析了下它们加载js的请求,终于明白了,它们都使用的无阻塞脚本加载技术,即使用script节点方式加载脚本,这样就很容易屏蔽js带来的阻塞问题了。

上面的实例中我使用script节点将脚本都是嵌入到head节点里,这个似乎和将脚本置于html文档末尾的原则不同,这个是不是需要改进了,答案是不需要改进,将脚本置于文档末尾目的是为了避免js的阻塞,而我们使用无阻塞脚本了,这个问题不是解决了吗?所以代码置于head标签还是html文档底部也就无关紧要了。

最后我要纠正一个错误的观点,页面加载的总时间是衡量页面加载快捷的标准吗?答案是,的确是个标准,但是不是最精确的标准,页面同步阻塞加载的时间才是衡量页面加载效率的准确标准,非阻塞脚本加载可能会增加整个页面加载的时间,但是它可以减少页面阻塞加载的时间,而页面阻塞才是影响用户体验的元凶,页面优化最重要的关注点就是你所看到的的东西要加载的更加快。

无阻塞脚本可以分割外部脚本的下载和执行操作,这是程序员使用的hack技术,它很酷,但是会导致程序的复杂度增加,可读性下降,所以它应该是web前端架构师的技术,日常开发我们要慎用它。

探真无阻塞加载javascript脚本技术,我们会发现很多意想不到的秘密,首发于 博客 - 伯乐在线

JavaScript代码复用模式

$
0
0

代码复用及其原则

代码复用,顾名思义就是对曾经编写过的代码的一部分甚至全部重新加以利用,从而构建新的程序。在谈及代码复用的时候,我们首先可以想到的是 继承性。代码复用的原则是:

优先使用对象组合,而不是类继承

在js中,由于没有类的概念,因此实例的概念也就没多大意义,js中的对象是简单的键-值对,可以动态的创建和修改它们。

但在 js中,我们可以使用构造函数和 new操作符来实例化一个对象,这与其他使用类的编程语言在语法上有其相似之处。

例如:

var trigkit4 = new Person();

js在调用构造函数 Person时似乎看起来是一个类,但其实际上仍然是一个函数,这让我们产生了一些假定在类的基础上的开发思路和继承模式,我们可以称之为“类式继承模式”。

传统的继承模式是需要 class关键字的,我们假定以上的类式继承模式为 现代继承模式,这是一种不需要以类的方式考虑的模式。

类式继承模式

看下面两个构造函数 Parent()Child()的例子:

<script type="text/javascript">
    function Parent(name){
        this.name = name || 'Allen';
    }

    Parent.prototype.say = function(){
        return this.name;
    }

    function Child(name){}

    //用Parent构造函数创建一个对象,并将该对象赋值给Child原型以实现继承
    function inherit(C,P){
        C.prototype = new P();//原型属性应该指向一个对象,而不是函数
    }

    //调用声明的继承函数
    inherit(Child,Parent);
</script>

当使用 new Child()语句创建一个对象时,它会通过原型从 Parent()实例获取它的功能,比如:

var kid = new Child();
kid.say();//Allen

原型链

讨论一下类式继承模式下原型链的工作原理,我们将对象看做是内存中某处的块,该内存块包含数据以及指向其他块的引用。当用 new Parent()语句创建一个对象时,就会创建如下图左边的这样一个块,这个块保存了 name属性,如果想访问 say()方法,我们可以通过指向构造函数 Parent()prototype(原型)属性的隐式链接 __proto__,便可访问右边区块 Parent.prototype

图片描述

那么,当使用 var kid = new Child()创建新对象时会发生什么?如下图:

图片描述

使用 new Child()语句所创建的对象除了隐式链接 __proto__外,它几乎是空的。这种情况下, __proto__指向了在 inherit()函数中使用 new Parent()语句所创建的对象

当执行 kid.say()时,由于最左下角的区块对象并没有 say()方法,因此他将通过原型链查询中间的区块对象,然而,中间的区块对象也没有 say()方法,因此他又顺着原型链查询到最右边的区块对象,而该对象正好有 say()方法。完了吗?

执行到这里的时候并没有完,在 say()方法中引用了 this.name,this指向构造函数所创建的对象,在这里,它指向了 new Child()这个区块,然而, new Child()中并没有 name属性,为此,将查询中间区块,而中间区块正好有 name属性,至此,原型链的查询完毕。

更详细的讨论请查看我这篇文章: JavaScript学习总结(五)原型和原型链详解

共享原型

本模式的法则在于:可复用的成员应该转移到原型中而不是放置在this中。因此,处于继承的目的,任何值得继承的东西都应该放在原型中实现。所以,可以将子对象的原型与父对象的原型设置为相同即可,如下示例所示:

function inherit(C,P){
    C.prototype = P.prototype;
}

图片描述

子对象和父对象共享同一个原型,并且可以同等的访问 say()方法。然而,子对象并没有继承 name属性

原型继承

原型继承是一种“现代”无类继承模式。看如下实例:

<script type="text/javascript">
    //要继承的对象
    var parent = {
        name : "Jack"  //这里不能有分号哦
    };

    //新对象
    var child = Object(parent);

    alert(child.name);//Jack
</script>

在原型模式中,并不需要使用对象字面量来创建父对象。如下代码所示,可以使用构造函数来创建父对象,这样做的话,自身的属性和构造函数的原型的属性都将被继承。

<script type="text/javascript">
    //父构造函数
    function Person(){
        this.name = "trigkit4";
    }
    //添加到原型的属性
    Person.prototype.getName = function(){
        return this.name;
    };

    //创建一个新的Person类对象
    var obj = new Person();

    //继承
    var kid = Object(obj);

    alert(kid.getName());//trigkit4
</script>

本模式中,可以选择仅继承现有构造函数的原型对象。对象继承自对象,而不论父对象是如何创建的,如下实例:

<script type="text/javascript">
    //父构造函数
    function Person(){
        this.name = "trigkit4";
    }
    //添加到原型的属性
    Person.prototype.getName = function(){
        return this.name;
    };

    //创建一个新的Person类对象
    var obj = new Person();

    //继承
    var kid = Object(Person.prototype);

    console.log(typeof kid.getName);//function,因为它在原型中
    console.log(typeof kid.name);//undefined,因为只有该原型是继承的
</script>

To be continued……

今天开始应该使用 5 个JavaScript调试技巧

$
0
0

我之前使用过用 printf debugging,自此之后我用这种方法似乎总能更快地解决bug。
在某些情况下需要更好的工具,下面是其中的一些佼佼者,我敢肯定你会发现它们的有用之处:

1. debugger;

正如我 之前提到的,你可以使用“debugger;”语句在代码中加入强制断点。
需要断点条件吗?只需将它包装它在IF子句中:

if (somethingHappens) {
    debugger;
}

只需记住在上线前移除。

2. 当节点变化时断开

有时DOM像有了自己的想法。当不可思议的变化发生时很难找到问题的根源。
Chrome开发人员工有调试这个问题的超级有用技能。这就是所谓的“Break on…”,你可以通过在元素选项卡上右键DOM节点找到它。

断点可以在节点被删除后设置,当节点的属性更改或者其子树中的节点变化时。
请输入图片描述

3. Ajax断点

XHR断点或我称作的Ajax断点,也允许当一个预期Ajax请求创建时断开。
当调试你的web应用的网络时这是个让人吃惊的工具。
请输入图片描述

4. 模拟不同的移动设备

Chrome增加了内置的移动设备模拟工具,这将简化你的日常工作。
选择任何非Console的选项卡找到它们,按键盘上的esc键并选择你想摸你的移动设备。

你当然不会得到一个真正的iPhone,但尺寸、触摸事件和agemt都会为你效仿。
请输入图片描述

5. 通过审核提升你的站点

YSlow是个伟大的工具。Chrome也在开发人员工具下包含一个称作Audits的类似工具。
使用快速审核一下你的网站,来获得有用实际的优化技巧。
请输入图片描述

还有什么呢?
没有这些工具我不能想象如何开发。当我发现新的后我会发布更多,敬请期待。

via WEB前端开发

JavaScript代码组织结构良好的5个特点[reuqire.js]

$
0
0

随着JavaScript项目的成长,如果你不小心处理的话,他们往往会变得难以管理。我们发现自己常常陷入的一些问题: 当在创建新的页面时发现,很难重用或测试之前写的代码。
当我们更深处地研究这些问题,我们发现根本原因是无效的依赖管理造成的。比如,脚本A依赖脚本B,并且脚本B又依赖脚本C,当C没有被正确引入时,整个依赖链就无法正常工作了。
为了解决这个问题,我们已经采取了异步模块定义(AMD)的模式,并引入require.js到我们的技术堆栈。经过对AMD的进一步探索,我们已经基本确定,组织严密的JavaScript一般都呈现以下五个特点:

  • 始终声明我们的依赖
  • 为第三方代码库添加shim(垫片)
  • 定义跟调用应该分离
  • 依赖应该异步加载
  • 模块不应依赖全局变量

让我们详细讨论一下。

始终声明我们的依赖

我们最常碰到了的一个问题是,我们会经常忽略那些会被确定加载的依赖项。举例来说,如果我们创建了一个jQuery插件,一般认为没有必要申报jQuery的依赖,因为它在大多数页面都是默认装载的。虽然这似乎适用于大多数的网页,但当我们试图进行单元测试或在一个全新的页面加载时,它就变成一个问题。
始终声明我们的依赖,我们就消除了JavaScript中90%的问题。可重用的代码变得更可靠,单元测试的数量增加了4倍也是一个因素。

为第三方代码库添加shim(垫片)

在管理JavaScript依赖时经常碰到的一个有趣问题是,较旧的第三方库可能无法和您的依赖关系管理系统配合工作。例如,你们内部使用了jQuery的一个很酷的插件,但它对require.js一无所知。这会成为一个问题,因为第一个特点,我们来添加对这个插件的引用。
解决的办法是通过依赖管理工具为这个插件制作一个垫片。在require.js中,这可以很容易地通过配置来完成:

var require = {"shim": {"lib/cool-plugin": {"deps": ["lib/jquery"]
        }
    }
}

有了这个简单的配置,每一个加载 lib/cool-plugin.js 的脚本都会自动加载jQuery。将有助于满足所有相关性.
最终的结果是代码更容易测试和重用,因为你总是有一个require()来调用所需的功能。

定义跟调用应该分离

这是限制JavaScript代码的可重用性和可测试性的一个常见问题。问题表现在一个单一的文件即定义了一个类/函数又调用了它。考虑下面的代码:

## js/User.js

define(functino(require) {
   var User = function(name, greeter) {
        this.name = name;
        this.greeter = greeter;
   };

   User.prototype.sayHello = function() {
        this.greeter("Hello, " + this.name);
   };

   var user = new User('Alice', window.alert);
   user.sayHello();
});

在这个例子中,一个单一的文件即定义了User类又调用它。这将很难重用这个代码,因为只要加载这个脚本就会出现alert。同样greeter这个非常难以测试。
解决的办法是保持定义和执行的分离。这有助于确保可重用性和可测性:

## js/User.js

define(functino(require) {
   var User = function(name, greeter) {
        this.name = name;
        this.greeter = greeter;
   };

   User.prototype.sayHello = function() {
        this.greeter("Hello, " + this.name);
   };

   return User;
});



## js/my-page.js

define(functino(require) {
    var User = require('js/User');
    var user = new User('Alice', window.alert);
    user.sayHello();
});

这种变化,User类可以安全地在许多脚本中重用。

依赖应该异步加载

因为试图同步加载脚本会导致浏览器锁死,这是非常重要的,你的脚本和你的模块应该使用异步加载机制。 Require.js在默认情况下,所有异步加载你的模块,只有所有的的依赖都加载完以后才会执行你的模块代码的函数。
通过使用一个闭包,我们可以进一步利 用“use strict”的好处

模块不应依赖全局变量

为了进一步加强我们的JavaScript代码库,我们已经(几乎)完全消灭了全局变量(除了由require.js提供的全局变量,如require()和define())。全局变量是臭名昭著的潜在的进入模块的“隐藏的依赖关系”,它会使代码很难重用或测试。
Require.js也让我们转换第三方全局变量,require() - 通过垫补功能能模块。在这个例子中,lib/calculator 创建一个全局的计算器对象,这个库是被require化的。

var require = {"shim" : {"lib/calculator": {"export": "Calc"
        }
    }
}

结论

管理依赖是挺难的(hard),但肯定不是做不到的(difficult)。通过使用依赖管理,你的代码将更可靠。

via ourjs

JavaScript继承方式详解

$
0
0

js继承的概念

js里常用的如下两种继承方式:

原型链继承(对象间的继承)
类式继承(构造函数间的继承)

由于 js不像 java那样是真正面向对象的语言, js是基于对象的,它没有类的概念。所以,要想实现继承,可以用 js的原型 prototype机制或者用 applycall方法去实现

在面向对象的语言中,我们使用 来创建一个 自定义对象。然而 js中所有事物都是对象,那么用什么办法来创建自定义对象呢?这就需要用到 js原型

我们可以简单的把 prototype看做是一个模版,新创建的自定义对象都是这个模版( prototype)的一个拷贝 (实际上不是拷贝而是链接,只不过这种链接是不可见,新实例化的对象内部有一个看不见的 __Proto__指针,指向原型对象)。

js可以通过构造函数和原型的方式模拟实现类的功能。 另外, js类式继承的实现也是依靠原型链来实现的。

原型式继承与类式继承

类式继承是在子类型构造函数的内部调用超类型的构造函数。
严格的类式继承并不是很常见,一般都是组合着用:

function Super(){
    this.colors=["red","blue"];
}

function Sub(){
    Super.call(this);
}

原型式继承是借助已有的对象创建新的对象,将子类的原型指向父类,就相当于加入了父类这条原型链

原型链继承

为了让子类继承父类的属性(也包括方法),首先需要定义一个构造函数。然后,将父类的新实例赋值给构造函数的原型。代码如下:

<script>
    function Parent(){
        this.name = 'mike';
    }

    function Child(){
        this.age = 12;
    }
    Child.prototype = new Parent();//Child继承Parent,通过原型,形成链条

    var test = new Child();
    alert(test.age);
    alert(test.name);//得到被继承的属性
    //继续原型链继承
    function Brother(){   //brother构造
        this.weight = 60;
    }
    Brother.prototype = new Child();//继续原型链继承
    var brother = new Brother();
    alert(brother.name);//继承了Parent和Child,弹出mike
    alert(brother.age);//弹出12
</script>

以上原型链继承还缺少一环,那就是 Object,所有的构造函数都继承自 Object。而继承 Object是自动完成的,并不需要我们自己手动继承,那么他们的从属关系是怎样的呢?

确定原型和实例的关系

可以通过两种方式来确定原型和实例之间的关系。操作符 instanceofisPrototypeof()方法:

alert(brother instanceof Object)//true
alert(test instanceof Brother);//false,test 是brother的超类
alert(brother instanceof Child);//true
alert(brother instanceof Parent);//true

只要是原型链中出现过的原型,都可以说是该原型链派生的实例的原型,因此, isPrototypeof()方法也会返回 true

js中,被继承的函数称为超类型(父类,基类也行),继承的函数称为子类型(子类,派生类)。使用原型继承主要由两个问题:
一是字面量重写原型会中断关系,使用引用类型的原型,并且子类型还无法给超类型传递参数。

伪类解决引用共享和超类型无法传参的问题,我们可以采用“ 借用构造函数”技术

借用构造函数(类式继承)

<script>
    function Parent(age){
        this.name = ['mike','jack','smith'];
        this.age = age;
    }

    function Child(age){
        Parent.call(this,age);
    }
    var test = new Child(21);
    alert(test.age);//21
    alert(test.name);//mike,jack,smith
    test.name.push('bill');
    alert(test.name);//mike,jack,smith,bill</script>

借用构造函数虽然解决了刚才两种问题,但没有原型,则复用无从谈起,所以我们需要 原型链+借用构造函数的模式,这种模式称为 组合继承

组合继承

<script>
    function Parent(age){
        this.name = ['mike','jack','smith'];
        this.age = age;
    }
    Parent.prototype.run = function () {
        return this.name  + ' are both' + this.age;
    };
    function Child(age){
        Parent.call(this,age);
    }
    Child.prototype = new Parent();
    var test = new Parent(21);//写new Child(21)也行
    alert(test.run());//mike,jack,smith are both21
</script>

组合式继承是比较常用的一种继承方法,其背后的思路是 使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又保证每个实例都有它自己的属性

原型式继承

这种继承借助原型并基于已有的对象创建新对象,同时还不用创建自定义类型的方式称为 原型式继承

<script>
     function obj(o){
         function F(){}
         F.prototype = o;
         return new F();
     }
    var box = {
        name : 'trigkit4',
        arr : ['brother','sister','baba']
    };
    var b1 = obj(box);
    alert(b1.name);//trigkit4

    b1.name = 'mike';
    alert(b1.name);//mike

    alert(b1.arr);//brother,sister,baba
    b1.arr.push('parents');
    alert(b1.arr);//brother,sister,baba,parents

    var b2 = obj(box);
    alert(b2.name);//trigkit4
    alert(b2.arr);//brother,sister,baba,parents
</script>

原型式继承首先在 obj()函数内部创建一个临时性的构造函数 ,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。

寄生式继承

这种继承方式是把原型式+ 工厂模式结合起来,目的是为了封装创建的过程。

<script>
    function create(o){
        var f= obj(o);
        f.run = function () {
            return this.arr;//同样,会共享引用
        };
        return f;
    }
</script>

组合式继承的小问题

组合式继承是 js最常用的继承模式,但组合继承的超类型在使用过程中会被调用两次;一次是创建子类型的时候,另一次是在子类型构造函数的内部

<script>
    function Parent(name){
        this.name = name;
        this.arr = ['哥哥','妹妹','父母'];
    }

    Parent.prototype.run = function () {
        return this.name;
    };

    function Child(name,age){
        Parent.call(this,age);//第二次调用
        this.age = age;
    }

    Child.prototype = new Parent();//第一次调用
</script>

以上代码是之前的组合继承,那么寄生组合继承,解决了两次调用的问题。

寄生组合式继承

<script>
    function obj(o){
        function F(){}
        F.prototype = o;
        return new F();
    }
    function create(parent,test){
        var f = obj(parent.prototype);//创建对象
        f.constructor = test;//增强对象
    }

    function Parent(name){
        this.name = name;
        this.arr = ['brother','sister','parents'];
    }

    Parent.prototype.run = function () {
        return this.name;
    };

    function Child(name,age){
        Parent.call(this,name);
        this.age =age;
    }

    inheritPrototype(Parent,Child);//通过这里实现继承

    var test = new Child('trigkit4',21);
    test.arr.push('nephew');
    alert(test.arr);//
    alert(test.run());//只共享了方法

    var test2 = new Child('jack',22);
    alert(test2.arr);//引用问题解决
</script>

call和apply

全局函数 applycall可以用来改变函数中 this的指向,如下:

 // 定义一个全局函数
    function foo() {
        console.log(this.fruit);
    }

    // 定义一个全局变量
    var fruit = "apple";
    // 自定义一个对象
    var pack = {
        fruit: "orange"
    };

    // 等价于window.foo();
    foo.apply(window);  // "apple",此时this等于window
    // 此时foo中的this === pack
    foo.apply(pack);    // "orange"

JS 和 CSS 的位置对其他资源加载顺序的影响

$
0
0

克军做了一系列测试: js和css的顺序关系,给出了现象和结论,但未给出原因。

JS 和 CSS 在页面中的位置,会影响其他资源(指 img 等非 js 和 css 资源)的加载顺序,究其原因,有三个值得注意的点:

  1. JS 有可能会修改 DOM.典型的,可能会有 document.write. 这意味着,在当前 JS 加载和执行完成前,后续所有资源的下载有可能是没必要的。这是 JS 阻塞后续资源下载的根本原因。
  2. JS 的执行有可能依赖最新样式。比如,可能会有 var width = $('#id').width(). 这意味着,JS 代码在执行前,浏览器必须保证在此 JS 之前的所有 css(无论外链还是内嵌)都已下载和解析完成。这是 CSS 阻塞后续 JS 执行的根本原因。
  3. 现代浏览器很聪明,会进行 prefetch 优化。性能是如此重要,现代浏览器在竞争中,在 UI update 线程之外,还会开启另一个线程,对后续 JS 和 CSS 提前下载(注意,仅提前下载,并不执行)。有了 prefetch 优化,这意味着,在不存在任何阻塞的情况下,理论上 JS 和 CSS 的下载时机都非常优先,和位置无关。

以上三点可简述为三条基本定律:

  • 定律一:资源是否下载依赖 JS 执行结果。
  • 定律二:JS 执行依赖 CSS 最新渲染。
  • 定律三:现代浏览器存在 prefetch 优化。

有了这三条定律,再来看克军的测试,就很清晰了:

a,b – head里出现外联js,无论如何放,css文件都不能和body里的请求并行

根据定律一和定律三,可以知道上面的结论不够正确。比如:

<head><link rel="stylesheet" href="mock.php?css1&sleep=2"><script src="mock.php?js&sleep=3"></script><link rel="stylesheet" href="mock.php?css2&sleep=4"></head><body><link rel="stylesheet" href="mock.php?css3&sleep=5"><img src="test.gif"></body>

在 Chrome 下的瀑布图是:

黄色条是 js 的,可以看出 img 的延时下载是由定律一决定的。

定律三则决定了所有 js/css 都是并行开始下载的。在 Firefox 10 下,prefetch 非常强悍,对 img 也会预加载,瀑布图如下:

调整一下 sleep 时间,还可以观察到定律二的威力:

<head><link rel="stylesheet" href="mock.php?css1&sleep=3"> <!-- 修改 sleep 值,使其大于 js 的 --><script src="mock.php?js&sleep=2"></script><link rel="stylesheet" href="mock.php?css2&sleep=4"></head><body><link rel="stylesheet" href="mock.php?css3&sleep=5"><img src="test.gif"></body>

瀑布图立刻发生了变化:

因为定律一,决定 img 的下载在 js 执行后。又因为定律二,决定 js 的执行在第一个 css 后。于是最后在瀑布图上体现出来,就是 img 的下载在第一个 css 后。

再来看克军的第二个结论:

c – head里的内联js只要在所有外联css前面,css文件可以和body里的请求并行(图2)

d – head里的内联js只要在任一外联css后面,css文件就不能和body里的请求并行(图1)

这个是定律二的威力。结论 c 是正确的,因为没有 css 会影响 js 的执行。结论 d 则不够正确。img 等其他资源,会在 js 前面的 css 下载完成后,以及 js 执行后,立刻开始下载。与头部中,js 位置之后的 css 没关系。

克军的其他结论都是对的,不多说。

注意1:Firefox 10 的 prefetch 有点奇怪,有时会对 img 进行 prefetch,有时则不会。有兴趣的可以进一步寻找规律。

注意2:上面的三个定律,是黑盒猜测,有兴趣的可以去阅读浏览器的源码,应该能找到更深层次的原因。

注意3:本文没有考虑 defer, async 属性的影响,这是另一个故事。

浏览器在迅速发展,很多总结,特别是书籍上的,很难与时俱进。大家应该像克军学习,多测试,多发现,这样得来的知识,才不会过时。这篇博客的总结,也肯定在未来甚至就在现在,已经存在错误。这些都无所谓,关键是 要懂得测试的方法和分析的思路,有了“渔”,才能更好地探求和拥有“鱼”

转自 岁月如歌


JavaScript编程注意事项、技巧大全

$
0
0

收藏自 JavaScript奇技淫巧45招

JavaScript是一个绝冠全球的编程语言,可用于Web开发、移动应用开发(PhoneGap、Appcelerator)、服务器端开发(Node.js和Wakanda)等等。JavaScript还是很多新手踏入编程世界的第一个语言。既可以用来显示浏览器中的简单提示框,也可以通过nodebot或nodruino来控制机器人。能够编写结构清晰、性能高效的JavaScript代码的开发人员,现如今已成了招聘市场最受追捧的人。

在这篇文章里,我将分享一些JavaScript的技巧、秘诀和最佳实践,除了少数几个外,不管是浏览器的JavaScript引擎,还是服务器端JavaScript解释器,均适用。

本文中的示例代码,通过了在Google Chrome 30最新版(V8 3.20.17.15)上的测试。

1、首次为变量赋值时务必使用var关键字

变量没有声明而直接赋值得话,默认会作为一个新的全局变量,要尽量避免使用全局变量。

2、使用===取代==

==和!=操作符会在需要的情况下自动转换数据类型。但===和!==不会,它们会同时比较值和数据类型,这也使得它们要比==和!=快。

[10] === 10    // is false
[10]  == 10    // is true'10' == 10     // is true'10' === 10    // is false
[]   == 0     // is true
[] ===  0     // is false'' == false   // is true but true == "a" is false'' === false  // is false

3、underfined、null、0、false、NaN、空字符串的逻辑结果均为false

4、行尾使用分号

实践中最好还是使用分号,忘了写也没事,大部分情况下JavaScript解释器都会自动添加。对于为何要使用分号,可参考文章JavaScript中关于分号的真相。

5、使用对象构造器

function Person(firstName, lastName){
    this.firstName =  firstName;
    this.lastName = lastName;
}
var Saad = new Person("Saad", "Mousliki");

6、小心使用typeof、instanceof和contructor

typeof:JavaScript一元操作符,用于以字符串的形式返回变量的原始类型,注意,typeof null也会返回object,大多数的对象类型(数组Array、时间Date等)也会返回object
contructor:内部原型属性,可以通过代码重写
instanceof:JavaScript操作符,会在原型链中的构造器中搜索,找到则返回true,否则返回false

var arr = ["a", "b", "c"];
typeof arr;   // 返回 "object" 
arr instanceof Array // true
arr.constructor();  //[]

7、使用自调用函数

函数在创建之后直接自动执行,通常称之为自调用匿名函数(Self-Invoked Anonymous Function)或直接调用函数表达式(Immediately Invoked Function Expression )。格式如下:

(function(){
    // 置于此处的代码将自动执行
})();  
(function(a,b){
    var result = a+b;
    return result;
})(10,20)

8、从数组中随机获取成员

var items = [12, 548 , 'a' , 2 , 5478 , 'foo' , 8852, , 'Doe' , 2145 , 119];
var  randomItem = items[Math.floor(Math.random() * items.length)];

9、获取指定范围内的随机数

这个功能在生成测试用的假数据时特别有数,比如介与指定范围内的工资数。

var x = Math.floor(Math.random() * (max - min + 1)) + min;

10、生成从0到指定值的数字数组

var numbersArray = [] , max = 100;
for( var i=1; numbersArray.push(i++) < max;);  // numbers = [1,2,3 ... 100]

11、生成随机的字母数字字符串

function generateRandomAlphaNum(len) {
    var rdmString = "";
    for( ; rdmString.length < len; rdmString  += Math.random().toString(36).substr(2));
    return  rdmString.substr(0, len);
}

12、打乱数字数组的顺序

var numbers = [5, 458 , 120 , -215 , 228 , 400 , 122205, -85411];
numbers = numbers.sort(function(){ return Math.random() - 0.5});
/* numbers 数组将类似于 [120, 5, 228, -215, 400, 458, -85411, 122205]  */

这里使用了JavaScript内置的数组排序函数,更好的办法是用专门的代码来实现(如Fisher-Yates算法),可以参见StackOverFlow上的这个讨论。

13、字符串去空格

Java、C#和PHP等语言都实现了专门的字符串去空格函数,但JavaScript中是没有的,可以通过下面的代码来为String对象函数一个trim函数:

String.prototype.trim = function(){return this.replace(/^\s+|\s+$/g, "");};

新的JavaScript引擎已经有了trim()的原生实现。

14、数组之间追加

var array1 = [12 , "foo" , {name "Joe"} , -2458];
var array2 = ["Doe" , 555 , 100];
Array.prototype.push.apply(array1, array2);
/* array1 值为  [12 , "foo" , {name "Joe"} , -2458 , "Doe" , 555 , 100] */

15、对象转换为数组

var argArray = Array.prototype.slice.call(arguments);

16、验证是否是数字

function isNumber(n){
    return !isNaN(parseFloat(n)) && isFinite(n);
}

17、验证是否是数组

function isArray(obj){
    return Object.prototype.toString.call(obj) === '[object Array]' ;
}

但如果toString()方法被重写过得话,就行不通了。也可以使用下面的方法:

Array.isArray(obj); // its a new Array method

如果在浏览器中没有使用frame,还可以用instanceof,但如果上下文太复杂,也有可能出错。

var myFrame = document.createElement('iframe');
document.body.appendChild(myFrame);
var myArray = window.frames[window.frames.length-1].Array;
var arr = new myArray(a,b,10); // [a,b,10]  
// myArray 的构造器已经丢失,instanceof 的结果将不正常
// 构造器是不能跨 frame 共享的
arr instanceof Array; // false

18、获取数组中的最大值和最小值

var  numbers = [5, 458 , 120 , -215 , 228 , 400 , 122205, -85411]; 
var maxInNumbers = Math.max.apply(Math, numbers); 
var minInNumbers = Math.min.apply(Math, numbers);

19、清空数组

var myArray = [12 , 222 , 1000 ];  
myArray.length = 0; // myArray will be equal to [].

20、不要直接从数组中delete或remove元素

如果对数组元素直接使用delete,其实并没有删除,只是将元素置为了undefined。数组元素删除应使用splice。

切忌:

var items = [12, 548 ,'a' , 2 , 5478 , 'foo' , 8852, , 'Doe' ,2154 , 119 ]; 
items.length; // return 11 
delete items[3]; // return true 
items.length; // return 11 
/* items 结果为 [12, 548, "a", undefined × 1, 5478, "foo", 8852, undefined × 1, "Doe", 2154, 119] */

而应:

var items = [12, 548 ,'a' , 2 , 5478 , 'foo' , 8852, , 'Doe' ,2154 , 119 ]; 
items.length; // return 11 
items.splice(3,1) ; 
items.length; // return 10 
/* items 结果为 [12, 548, "a", 5478, "foo", 8852, undefined × 1, "Doe", 2154, 119]
删除对象的属性时可以使用delete。

21、使用length属性截断数组

前面的例子中用length属性清空数组,同样还可用它来截断数组:

var myArray = [12 , 222 , 1000 , 124 , 98 , 10 ];  
myArray.length = 4; // myArray will be equal to [12 , 222 , 1000 , 124].

与此同时,如果把length属性变大,数组的长度值变会增加,会使用undefined来作为新的元素填充。length是一个可写的属性。

myArray.length = 10; // the new array length is 10 
myArray[myArray.length - 1] ; // undefined

22、在条件中使用逻辑与或

var foo = 10;  
foo == 10 && doSomething(); // is the same thing as if (foo == 10) doSomething(); 
foo == 5 || doSomething(); // is the same thing as if (foo != 5) doSomething();

逻辑或还可用来设置默认值,比如函数参数的默认值。

function doSomething(arg1){ 
    arg1 = arg1 || 10; // arg1 will have 10 as a default value if it’s not already set
}

23、使得map()函数方法对数据循环

var squares = [1,2,3,4].map(function (val) {  
    return val * val;  
}); 
// squares will be equal to [1, 4, 9, 16]

24、保留指定小数位数

var num =2.443242342;
num = num.toFixed(4);  // num will be equal to 2.4432

注意,toFixec()返回的是字符串,不是数字。

25、浮点计算的问题

0.1 + 0.2 === 0.3 // is false 
9007199254740992 + 1 // is equal to 9007199254740992
9007199254740992 + 2 // is equal to 9007199254740994

为什么呢?因为0.1+0.2等于0.30000000000000004。JavaScript的数字都遵循IEEE 754标准构建,在内部都是64位浮点小数表示,具体可以参见JavaScript中的数字是如何编码的.

可以通过使用toFixed()和toPrecision()来解决这个问题。

26、通过for-in循环检查对象的属性

下面这样的用法,可以防止迭代的时候进入到对象的原型属性中。

for (var name in object) {  
    if (object.hasOwnProperty(name)) { 
        // do something with name
    }  
}

27、逗号操作符

var a = 0; 
var b = ( a++, 99 ); 
console.log(a);  // a will be equal to 1 
console.log(b);  // b is equal to 99

28、临时存储用于计算和查询的变量

在jQuery选择器中,可以临时存储整个DOM元素。

var navright = document.querySelector('#right'); 
var navleft = document.querySelector('#left'); 
var navup = document.querySelector('#up'); 
var navdown = document.querySelector('#down');

29、提前检查传入isFinite()的参数

isFinite(0/0) ; // false
isFinite("foo"); // false
isFinite("10"); // true
isFinite(10);   // true
isFinite(undefined);  // false
isFinite();   // false
isFinite(null);  // true,这点当特别注意

30、避免在数组中使用负数做索引

var numbersArray = [1,2,3,4,5];
var from = numbersArray.indexOf("foo") ;  // from is equal to -1
numbersArray.splice(from,2);    // will return [5]

注意传给splice的索引参数不要是负数,当是负数时,会从数组结尾处删除元素。

31、用JSON来序列化与反序列化

var person = {name :'Saad', age : 26, department : {ID : 15, name : "R&D"} };
var stringFromPerson = JSON.stringify(person);
/* stringFromPerson 结果为 "{"name":"Saad","age":26,"department":{"ID":15,"name":"R&D"}}"   */
var personFromString = JSON.parse(stringFromPerson);
/* personFromString 的值与 person 对象相同  */

32、不要使用eval()或者函数构造器

eval()和函数构造器(Function consturctor)的开销较大,每次调用,JavaScript引擎都要将源代码转换为可执行的代码。

var func1 = new Function(functionCode);
var func2 = eval(functionCode);

33、避免使用with()

使用with()可以把变量加入到全局作用域中,因此,如果有其它的同名变量,一来容易混淆,二来值也会被覆盖。

34、不要对数组使用for-in

避免:

var sum = 0;  
for (var i in arrayNumbers) {  
    sum += arrayNumbers[i];  
}

而是:

var sum = 0;  
for (var i = 0, len = arrayNumbers.length; i < len; i++) {  
    sum += arrayNumbers[i];  
}

另外一个好处是,i和len两个变量是在for循环的第一个声明中,二者只会初始化一次,这要比下面这种写法快:

for (var i = 0; i < arrayNumbers.length; i++)

35、传给setInterval()和setTimeout()时使用函数而不是字符串

如果传给setTimeout()和setInterval()一个字符串,他们将会用类似于eval方式进行转换,这肯定会要慢些,因此不要使用:

setInterval('doSomethingPeriodically()', 1000);  
setTimeout('doSomethingAfterFiveSeconds()', 5000);

而是用:

setInterval(doSomethingPeriodically, 1000);  
setTimeout(doSomethingAfterFiveSeconds, 5000);

36、使用switch/case代替一大叠的if/else

当判断有超过两个分支的时候使用switch/case要更快一些,而且也更优雅,更利于代码的组织,当然,如果有超过10个分支,就不要使用switch/case了。

37、在switch/case中使用数字区间

其实,switch/case中的case条件,还可以这样写:

function getCategory(age) {  
    var category = "";  
    switch (true) {  
        case isNaN(age):  
            category = "not an age";  
            break;  
        case (age >= 50):  
            category = "Old";  
            break;  
        case (age <= 20):  
            category = "Baby";  
            break;  
        default:  
            category = "Young";  
            break;  
    };  
    return category;  
}  
getCategory(5);  // 将返回 "Baby"

38、使用对象作为对象的原型

下面这样,便可以给定对象作为参数,来创建以此为原型的新对象:

function clone(object) {  
    function OneShotConstructor(){}; 
    OneShotConstructor.prototype = object;  
    return new OneShotConstructor(); 
} 
clone(Array).prototype ;  // []

39、HTML字段转换函数

function escapeHTML(text) {  
    var replacements= {"<": "<", ">": ">","&": "&", "\"": """};                      
    return text.replace(/[<>&"]/g, function(character) {  
        return replacements[character];  
    }); 
}

40、不要在循环内部使用try-catch-finally

try-catch-finally中catch部分在执行时会将异常赋给一个变量,这个变量会被构建成一个运行时作用域内的新的变量。

切忌:

var object = ['foo', 'bar'], i;  
for (i = 0, len = object.length; i <len; i++) {  
    try {  
        // do something that throws an exception 
    }  
    catch (e) {   
        // handle exception  
    } 
}

而应该:

var object = ['foo', 'bar'], i;  
try { 
    for (i = 0, len = object.length; i <len; i++) {  
        // do something that throws an exception 
    } 
} 
catch (e) {   
    // handle exception  
}

41、使用XMLHttpRequests时注意设置超时

XMLHttpRequests在执行时,当长时间没有响应(如出现网络问题等)时,应该中止掉连接,可以通过setTimeout()来完成这个工作:

var xhr = new XMLHttpRequest (); 
xhr.onreadystatechange = function () {  
    if (this.readyState == 4) {  
        clearTimeout(timeout);  
        // do something with response data 
    }  
}  
var timeout = setTimeout( function () {  
    xhr.abort(); // call error callback  
}, 60*1000 /* timeout after a minute */ ); 
xhr.open('GET', url, true);  
xhr.send();

同时需要注意的是,不要同时发起多个XMLHttpRequests请求。

42、处理WebSocket的超时

通常情况下,WebSocket连接创建后,如果30秒内没有任何活动,服务器端会对连接进行超时处理,防火墙也可以对单位周期没有活动的连接进行超时处理。

为了防止这种情况的发生,可以每隔一定时间,往服务器发送一条空的消息。可以通过下面这两个函数来实现这个需求,一个用于使连接保持活动状态,另一个专门用于结束这个状态。

var timerID = 0; 
function keepAlive() { 
    var timeout = 15000;  
    if (webSocket.readyState == webSocket.OPEN) {  
        webSocket.send('');  
    }  
    timerId = setTimeout(keepAlive, timeout);  
}  
function cancelKeepAlive() {  
    if (timerId) {  
        cancelTimeout(timerId);  
    }  
}

keepAlive()函数可以放在WebSocket连接的onOpen()方法的最后面,cancelKeepAlive()放在onClose()方法的最末尾。

43、时间注意原始操作符比函数调用快,使用VanillaJS

比如,一般不要这样:

var min = Math.min(a,b); 
A.push(v);

可以这样来代替:

var min = a < b ? a : b; 
A[A.length] = v;

44、开发时注意代码结构,上线前检查并压缩JavaScript代码

可以使用JSLint或JSMin等工具来检查并压缩代码。

编写更加稳定、可读性强的JavaScript代码

$
0
0

每个人都有自己的编程风格,也无可避免的要去感受别人的编程风格——修改别人的代码。”修改别人的代码”对于我们来说的一件很痛苦的事情。因为有些代码并不是那么容易阅读、可维护的,让另一个人来修改别人的代码,或许最终只会修改一个变量,调整一个函数的调用时机,却需要花上1个小时甚至更多的时间来阅读、缕清别人的代码。本文一步步带你重构一段获取位置的”组件”——提升你的javascript代码的可读性和稳定性。

本文内容如下:

  • 分离你的javascript代码
  • 函数不应该过分依赖外部环境
  • 语义化和复用
  • 组件应该关注逻辑,行为只是封装
  • 形成自己的风格的代码

分离你的javascript代码

下面一段代码演示了难以阅读/修改的代码:

(function (window, namespace) {
    var $ = window.jQuery;
    window[namespace] = function (targetId, textId) {
        //一个尝试复用的获取位置的"组件"
        var $target = $('#' + targetId),//按钮
            $text = $('#' + textId);//显示文本
        $target.on('click', function () {
            $text.html('获取中');
            var data = '北京市';//balabala很多逻辑,伪代码,获取得到位置中
            if (data) {
                $text.html(data);
            } else
                $text.html('获取失败');
        });
    }
})(window, 'linkFly');

这一段代码,我们暂且认可它已经构成一个”组件”。
上面的代码就是典型的一个方法搞定所有事情,一旦填充上内部的逻辑就会变得生活不能自理,而一旦增加需求,例如获取位置返回的数据格式需要加工,那么就要去里面寻找处理数据的代码然后修改。

我们分离一下逻辑,得到代码如下:

(function (window, namespace) {
    var $ = window.jQuery,
        $target,
        $text,
        states= ['获取中', '获取失败'];
    function done(address) {//获取位置成功
        $text.html(address);
    }
    function fail() {
        $text.html(states[1]);
    }
    function checkData(data) {
        //检查位置信息是否正确
        return !!data;
    }
    function loadPosition() {
        var data = '北京市';//获取位置中
        if (checkData(data)) {
            done(data);
        } else
            fail();
    }
    var init = function () {
        $target.on('click', function () {
            $text.html(states[0]);
            loadPosition();
        });
    };
    window[namespace] = function (targetId, textId) {
        $target = $('#' + targetId);
        $text = $('#' + textId);
        initData();
        setData();
    }
})(window, 'linkFly');

函数不应该过分依赖外部环境

上面的代码中,我们已经把整个组件,切割成了各种函数(注意这里我说的是函数,不是方法),这里常出现一个新的问题:函数过分依赖不可控的变量。

变量$target和$text身为环境中的全局变量,从组件初始化便赋值,而我们切割后的代码大多数的操作方法都依赖$text,尤其是$text和done()、fail()之间暧昧的关系,一旦$text相关的结构、逻辑改变,那么我们的代码将会进行不小的改动。

和页面/DOM相关的都是不可信赖的(例如$target和$text),一旦页面结构发生改变,它的行为很大程度上也会随之改变。而函数也不应该依赖外部的环境。
在不可控的变量上,我们应该解开函数和依赖变量上的关系,让函数变得更加专注自己区域的逻辑,更加的纯粹。简单的说:函数所依赖的外部变量,都应该通过参数传递到函数内部。
新的代码如下:

(function (window, namespace) {
    var $ = window.jQuery;
    //检查位置信息是否正确
    function checkData(data) {
        return !!data;
    }
    //获取位置中
    function loadPosition(done, fail) {
        var data = '北京市';//获取位置中
        if (checkData(data)) {
            done(data);
        } else
            fail();
    }
    window[namespace] = function (targetId, textId) {
       var  $target = $('#' + targetId),
            $text = $('#' + textId);
        var states = ['获取中', '获取失败'];
        $target.on('click', function () {
            $text.html(states[0]);
            loadPosition(function (address) {//获取位置成功
                $text.html(address);
            }, function () {//获取位置失败
                $text.html(states[1]);
            });
        });
    }
})(window, 'linkFly');

语义化和复用

变量states是一个数组,它描述的行为难以阅读,每次看到states[0]都有一种分分钟想捏死原作者的冲动,因为我们总是要记住变量states的值,在代码上,我们应该尽可能让它可以很好的被阅读。

另外,上面的代码中$text.html就是典型的代码重复,我们再一次的修改代码,请注意这一次修改的代码中,我们所抽离的changeStateText()的代码位置,它并没有被提升到上一层环境中(也就是整个大闭包的环境)。

(function (window, namespace) {
    var $ = window.jQuery;
    function checkData(data) {
        return !!data;
    }
    function loadPosition(done, fail) {
        var data = '北京市';//获取位置中
        if (checkData(data)) {
            done(data);
        } else
            fail();
    }
    window[namespace] = function (targetId, textId) {
        var $target = $('#' + targetId),
            $text = $('#' + textId),
            changeEnum = { LOADING: '获取中', FAIL: '获取失败' },
            changeStateText = function (text) {
                $text.html(text);
            };
        $target.on('click', function () {
            changeStateText(changeEnum.LOADING);
            loadPosition(function (address) {
                changeStateText(address);
            }, function () {
                changeStateText(changeEnum.FAIL);
            });
        });
    }
})(window, 'linkFly');

提及语义化,我们必须要知道当前整个代码的逻辑和语义:

在这整个组件中,所有的函数模块可以分为:工具和工具提供者。

上一层环境(整个大闭包)在我们的业务中扮演着工具的身份,它的任务是缔造一套和获取位置逻辑相关的工具,而在window[namespace])函数中,则是工具提供者的身份,它是唯一的入口,负责提供组件完整的业务给工具的使用者。
这里的$text.html()在逻辑上并不属于工具,而是属于工具提供者使用工具后所得到的反馈,所以changeStateText()函数置于工具提供者window[namespace]()中。

组件应该关注逻辑,行为只是封装

到此为止,我们分离了函数,并让这个组件拥有了良好的语义。但这时候来了新的需求:当没有获取到位置的时候,需要进行一些其他的操作。这时候会发现,我们需要window[namespace]()上加上新的参数。
当我们加上新的参数之后,又被告知新的需求:当获取位置失败了之后,需要修改一些信息,然后再次尝试获取位置信息。
不过幸好,我们的代码已经把大部分的逻辑抽离到了工具提供者中了,对整个工具的逻辑影响并不大。
同时我们再看看代码就会发现我们的组件除了工具提供者之外,没有方法(依赖在对象上的函数)。也就是说,我们的组件并没有对象。

我见过很多人的代码总是喜欢打造工具提供者,而忽略了工具的本质。迎合上面的增加的需求,那么我们的工具提供者将会变得越来越重,这时候我们应该思考到:是不是应该把工具提供出去?

让我们回到最初的需求——仅仅只是一个获取位置的组件,没错,它的核心业务就是获取位置——它不应该被组件化。它的本质应该是个工具对象,而不应该和页面相关,我们从一开始就不应该关注页面上的变化,让我们重构代码如下:

(function (window, namespace) {
    var Gps = {
        load: function (fone, fail) {
            var data = '北京市';//获取位置伪代码
            this.check(data) ?
                done(data, Gps.state.OK) :
                fail(Gps.state.FAIL);
        },
        check: function (data) {
            return !!data;
        },
        state: { OK: 1, FAIL: 0 }
    };
    window[namespace] = Gps;
})(window, 'Gps');

在这里,我们直接捏死了工具提供者,我们直接将工具提供给外面的工具使用者,让工具使用者直接使用我们的工具,这里的代码无关状态、无关页面。

至此,重构完成。

形成自己风格的代码

之所以讲这个是因为大家都有自己的编程风格。有些人的编程风格就是开篇那种代码的…
我觉得形成自己的编程风格,是建立在良好代码的和结构/语义上的。否则只会让你的代码变得越来越难读,越来越难写。
****
单var和多var
我个人是喜欢单var风格的,不过我觉得代码还是尽可能在使用某一方法/函数使用前进行var,有时候甚至于为了单var而变得丧心病狂:由于我又过分的喜爱 函数表达式声明,函数表达式声明并不会在var语句中执行,于是偶尔会出现这种边声明边执行的代码,为了不教坏小朋友就不贴代码了(我不会告诉你们其实是我找不到了)。

对象属性的屏蔽
下面的代码演示了两种对象的构建,后一种通过闭包把内部属性隐藏,同样,两种方法都实现了无new化,我个人…是不喜欢看见很多this的..但还是推荐前者。

(function () {
    //第一种,曝露了_name属性
    var Demo = function () {
        if (!(this instanceof Demo))
            return new Demo();
        this._name = 'linkFly';
    };
    Demo.prototype.getName = function () {
        return this._name;
    }

    //第二种,多一层闭包意味内存消耗更大,但是屏蔽了_name属性
    var Demo = function () {
        var name = 'linkFly';
        return {
            getName: function () {
                return name;
            }
        }
    }
});

巧用变量置顶[hoisting]
巧用函数声明的 变量置顶特性意味着处女座心态的你放弃单var,但却可以让你的函数在代码结构上十分清晰,例如下面的代码:

(function () {
    var names = [];
    return function (name) {
        addName(name);
    }
    function addName(name) {
        if (!~names.indexOf(name))//如果存在则不添加
            names.push(name);
        console.log(names);// ["linkFly"]
    }
}())('linkFly');

if和&&
这种代码,在几个群里都见过讨论:

(function () {
    var key = 'linkFly',
        cache = { 'linkFly': 'http://www.cnblogs.com/silin6/' },
        value;
    //&&到底
    key && cache && cache[key] && (value = cache[key]);
    //来个if
    if (key && cache && cache[key])
        value = cache[key];
})();

大概就想到这么些了,我突然发现我不太推荐的代码,都是我写的代码,囧。如果各位也还有更多有趣的代码,希望各位看官能掏出来让小弟见识见识。

JavaScript中的各种宽高以及位置总结

$
0
0

在javascript中操作dom节点让其运动的时候,常常会涉及到各种宽高以及位置坐标等概念,如果不能很好地理解这些属性所代表的意义,就不能理解js的运动原理,同时,由于这些属性概念较多,加上浏览器之间

实现方式不同,常常会造成概念混淆,经过研究之后,这里来进行一个总结。

第一部分:DOM对象

1.1只读属性

所谓的只读属性指的是DOM节点的固有属性,该属性只能通过js去获取而不能通过js去设置,而且获取的值是只有数字并不带单位的(px,em等),如下:

1)clientWidth和clientHeight

该属性指的是元素的可视部分宽度和高度,即padding+content,如果没有滚动条,即为元素设定的高度和宽度,如果出现滚动条,滚动条会遮盖元素的宽高,那么该属性就是其本来宽高减去滚动条的宽高

css:

<style>
        .one{
            width: 200px;
            height: 200px;
            background: red;
            border: 1px solid #000000;
            overflow: auto;
        }</style>

HTML

<body><div class="one">
    javascript高级应用<br>
    javascript高级应用<br>
    javascript高级应用<br>
    javascript高级应用<br>
    javascript高级应用<br>
    javascript高级应用<br>
    javascript高级应用<br>
    javascript高级应用<br>
    javascript高级应用<br>
    javascript高级应用<br>
    javascript高级应用<br>
    javascript高级应用<br>
    javascript高级应用<br>
    javascript高级应用<br>
    javascript高级应用<br>
    javascript高级应用<br>
    javascript高级应用<br>
    javascript高级应用<br></div></body>

js

<script>
    window.onload=function(){
        var oDiv=document.getElementsByTagName('div')[0];
        console.log(oDiv.clientWidth+":"+oDiv.clientHeight);
    }</script>

结果:

clipboard.png

元素本来设定为宽高都是200,即可视部分宽高都是200,但是由于出现了垂直方向的滚动条,占据了可视部分的宽度,所以clientWidth变成了183,而clientHeight依然是200.

2)offsetWidth和offsetHeight

这一对属性指的是元素的border+padding+content的宽度和高度,该属性和其内部的内容是否超出元素大小无关,只和本来设定的border以及width和height有关

css和HTML部分同上,js部分如下:

<script>
    window.onload=function(){
        var oDiv=document.getElementsByTagName('div')[0];
        console.log(oDiv.offsetWidth+":"+oDiv.offsetHeight);
    }</script>

最终结果:

可以看到该属性和clientWidth以及clientHeight相比,多了设定的边框border的宽度和高度。

3)clientTop和clientLeft

这一对属性是用来读取元素的border的宽度和高度的。

css

<style>
    body{
        border: 2px solid #000000;
    }
    .one{
        border: 1px solid red;
        width: 100px;
        height: 100px;
        background: red;
    }</style>

html

<body><div class="one"></div></body>

js

<script>
    var oDiv=document.getElementsByClassName('one')[0];
    console.log(oDiv.clientLeft+":"+oDiv.clientTop);</script>

最终结果:

clipboard.png

 可以看到div的border被设定了1px的宽,这里显示的就是它的宽度

4)offsetLeft和offsetTop

说到这对属性就需要说下offsetParent,所谓offsetParent指的是当前元素的离自己最近的具有定位的(position:absolute或者position:relative)父级元素(不仅仅指的是直接父级元素,只要是它的父元素都可以),该父级元素就是当前元素的offsetParent,如果从该元素向上寻找,找不到这样一个父级元素,那么当前元素的offsetParent就是body元素。而offsetLeft和offsetTop指的是当前元素,相对于其offsetParent左边距离和上边距离,即当前元素的border到包含它的offsetParent的border的距离如下所示:

css

<style>
    .two{
        position: relative;
        width: 200px;
        height: 200px;
        border: 1px solid green;
    }
    .one {
        width: 100px;
        height: 100px;
        background: red;
        margin: 20px;
        border: 1px solid #000000;
        position: absolute;
        top:20px;
    }</style>

HTML

<body><div class="two"><div class="one"></div></div></body>

js

<script>
    var oDiv=document.querySelector('.one');
    console.log(oDiv.offsetTop+":"+oDiv.offsetLeft);</script>

最终结果:

clipboard.png

 这里让div.two相对定位,让div.one绝对定位,所以div.two是one的offsetParent,同时,又给div.one加了一个margin为20px,所以这里它的offsetTop为40,offsetLeft 本来为0,但是加上margin为20之后就变成了20.

5)scrollHeight和scrollWidth

顾名思义,这两个属性指的是当元素内部的内容超出其宽度和高度的时候,元素内部内容的实际宽度和高度,需要注意的是,当元素其中内容没有超过其高度或者宽度的时候,该属性是取不到的。

css

<style>
    .one{
        width: 100px;
        height: 100px;
        overflow: auto;
    }</style>

HTML

<body><div class="one">我是内容<br>我是内容<br>我是内容<br>我是内容<br>我是内容<br>我是内容<br>我是内容<br>我是内容<br>我是内容<br>我是内容<br>我是内容<br>我是内容<br>我是内容<br></div></body>

js

<script>
    var oDiv=document.querySelector('.one');
    oDiv.onscroll=function(){
        console.log(this.scrollHeight+":"+this.scrollWidth);
    }</script>

最终结果

clipboard.png

 尽管该div的宽高都是100,但是其scrollheight为234显示的是其中内容的实际高度,scrollWidth为83(由于滚动条占据了宽度)

1.2可读可写属性

所谓的可读可写属性指的是不仅能通过js获取该属性的值,还能够通过js为该属性赋值。

1)scrollTop和scrollLeft

这对属性是可读写的,指的是当元素其中的内容超出其宽高的时候,元素被卷起的高度和宽度。

css和html部分同上,js部分如下:

<script>
    var oDiv=document.querySelector('.one');
    oDiv.onscroll=function(){
        console.log(this.scrollTop+":"+this.scrollLeft);
    }</script>

最终结果:

clipboard.png

由于拖动了滚动条,scrollTop的属性值一直在增大,而水平方向没有滚动条,所以scrollLeft一直为0.

该属性还可以通过赋值内容自动滚动到某个位置,js如下:

<script>
    var oDiv=document.querySelector('.one');
    oDiv.scrollTop=20;
    oDiv.onscroll=function(){
        console.log(this.scrollTop+":"+this.scrollLeft);
    }</script>

结果如下:

clipboard.png

通过直接设定div的scrollTop,让它直接显示在滚动条在20垂直方向上20的位置。

2)obj.style.*属性

对于一个dom元素,它的style属性返回的是一个对象,这个对象中的任意一个属性是可读写的。如obj.style.top,obj.style.wdith等,在读的时候,他们返回的值常常是带有单位的(如px),同时,对于这种方式,

它只能够获取到该元素的行内样式,而并不能获取到该元素最终计算好的样式,这就是在读取属性值得时候和以上只读属性的区别,要获取计算好的样式,请使用obj.currentstyle(IE)和getComputedStyle(IE之外的浏览器)。另一方面,这些属性能够被赋值,js运动的原理就是通过不断修改这些属性的值而达到其位置改变的,需要注意的是,给这些属性赋值的时候需要带单位的要带上单位,否则不生效。

第二部分 Event对象

在js中,对于元素的运动的操作通常都会涉及到event对象,而event对象也存在很多位置属性,且由于浏览器兼容性问题会导致这些属性间相互混淆,这里一一讲解。

1)clientX和clientY,这对属性是当事件发生时,鼠标点击位置相对于浏览器(可视区)的坐标,即浏览器左上角坐标的(0,0),该属性以浏览器左上角坐标为原点,计算鼠标点击位置距离其左上角的位置,

不管浏览器窗口大小如何变化,都不会影响点击位置的坐标。

js

<script> var oDiv=document.querySelector('.one'); oDiv.onclick=function(ev){ var evt=ev||event; console.log(evt.clientX+":"+evt.clientY); }</script>

结果:

clipboard.png

2)screenX和screenY是事件发生时鼠标相对于屏幕的坐标,以设备屏幕的左上角为原点,事件发生时鼠标点击的地方即为该点的screenX和screenY值,如下所示:

clipboard.png

可以看到尽管浏览器窗口被缩到很小,但是坐标值却很大,因为是相对于屏幕坐标而不是浏览器的坐标。

3)offsetX和offsetY

这一对属性是指当事件发生时,鼠标点击位置相对于该事件源的位置,即点击该div,以该div左上角为原点来计算鼠标点击位置的坐标,如下所示:

clipboard.png

可以看到,点击该div的靠近左上角处,它的offsetX和offsetY为1,0,需要注意的是,IE,chrome,opera都支持该属性,唯独Firefox不支持该属性,Firefox中与此属性相对应的概念是,event.layerX和event.layerY,所以需要兼容浏览器时,获取鼠标点击位置相对于事件源的坐标的兼容写法为var disX=event.offsetX||event.layerX.

4)pageX和pageY

顾名思义,该属性是事件发生时鼠标点击位置相对于页面的位置,通常浏览器窗口没有出现滚动条时,该属性和event.clientX及event.clientY是等价的,但是当浏览器出现滚动条的时候,pageX通常会大于clientX,因为页面还存在被卷起来的部分的宽度和高度,如下所示:

clipboard.png

由于浏览器出现了垂直和水平的滚动条,所以pageX和pageY大于clientX和clientY。

当浏览器的滚动条没有被拖动或者浏览器没有滚动条的时候,两者是相等的。

clipboard.png

文章转自 js中的各种宽高以及位置总结

开发一个完整的JavaScript组件

$
0
0

作为一名开发者,大家应该都知道在浏览器中存在一些内置的控件:Alert,Confirm等,但是这些控件通常根据浏览器产商的不同而形态各异,视觉效果往往达不到UI设计师的要求。更重要的是,这类内置控件的风格很难与形形色色的各种风格迥异的互联网产品的设计风格统一。因此,优秀的前端开发者们各自开发自己的个性化控件来替代浏览器内置的这些控件。当然,这类组件在网络上已经有不计其数相当优秀的,写这篇文章的目的不是为了说明我开发的这个组件有多优秀,也不是为了炫耀什么,只是希望通过这种方式,与更多的开发者互相交流,互相学习,共同进步。好,废话不多说,言归正传。

功能介绍

  • 取代浏览器自带的Alert、Confirm控件
  • 自定义界面样式
  • 使用方式与内置控件基本保持一致

效果预览

1、Alert控件

2、Confirm控件

3、完整代码, 在线预览(见底部,提供压缩包下载)

开发过程

1. 组件结构设计

首先,我们来看下内置组件的基本使用方法:

1 alert("内置Alert控件");
2 if (confirm("关闭内置Confirm控件?")) {
3     alert("True");
4 } else {
5     alert("False");
6 }

为了保证我们的组件使用方式和内置控件保持一致,所以我们必须考虑覆盖内置控件。考虑到组件开发的风格统一,易用,易维护,以及面向对象等特性,我计划将自定义的alert和confirm方法作为一个类(Winpop)的实例方法,最后用实例方法去覆盖系统内置控件的方法。为了达到目的,我的基本做法如下:

1 var obj = new Winpop(); // 创建一个Winpop的实例对象
2 // 覆盖alert控件
3 window.alert = function(str) {
4     obj.alert.call(obj, str);
5 };
6 // 覆盖confirm控件
7 window.confirm = function(str, cb) {
8     obj.confirm.call(obj, str, cb);
9 };

需要注意的是,由于浏览器内置的控件可以阻止浏览器的其他行为,而我们自定义的组件并不能具备这种能力,为了尽可能的做到统一,正如预览图上看到的,我们在弹出自定义组件的时候使用了一个全屏半透明遮罩层。也正是由于上述原因,confirm组件的使用方式也做了一些细微的调整,由内置返回布尔值的方式,改为使用回调函数的方式,以确保可以正确的添加“确定”和“取消”的逻辑。因此,自定义组件的使用方式就变成了下面这种形式:

1 alert("自定义Alert组件");
2 confirm("关闭自定义Confirm组件?", function(flag){
3     if (flag) {
4         alert("True");
5     } else {
6         alert("False");
7     }
8 });

2. 组件代码设计

在正式介绍Winpop组件的代码之前,我们先来看一下一个Javascript组件的基本结构:

 1 (function(window, undefined) {
 2     function JsClassName(cfg) {
 3         var config = cfg || {};
 4         this.get = function(n) {
 5             return config[n];
 6         }
 7         this.set = function(n, v) {
 8             config[n] = v;
 9         }
10         this.init();
11     }
12     JsClassName.prototype = {
13         init: function(){},
14         otherMethod: function(){}
15     };
16     window.JsClassName = window.JsClassName || JsClassName;
17 })(window);

使用一个自执行的匿名函数将我们的组件代码包裹起来,尽可能的减少全局污染,最后再将我们的类附到全局window对象上,这是一种比较推荐的做法。

构造函数中的get、set方法不是必须的,只是笔者的个人习惯而已,觉得这样写可以将配置参数和其他组件内部全局变量缓存和读取的调用方式统一,似乎也更具有面向对象的型。欢迎读者们说说各自的想法,说说这样写到底好不好。

接下来我们一起看下Winpop组件的完整代码:

  1 (function(window, jQuery, undefined) {
  2 
  3     var HTMLS = {
  4         ovl: '<div id="J_WinpopMask"></div>' + '<div id="J_WinpopBox">' + '<div></div>' + '<div></div>' + '</div>',
  5         alert: '<input type="button" value="确定">',
  6         confirm: '<input type="button" value="取消">' + '<input type="button" value="确定">'
  7     }
  8 
  9     function Winpop() {
 10         var config = {};
 11         this.get = function(n) {
 12             return config[n];
 13         }
 14 
 15         this.set = function(n, v) {
 16             config[n] = v;
 17         }
 18         this.init();
 19     }
 20 
 21     Winpop.prototype = {
 22         init: function() {
 23             this.createDom();
 24             this.bindEvent();
 25         },
 26         createDom: function() {
 27             var body = jQuery("body"),
 28                 ovl = jQuery("#J_WinpopBox");
 29 
 30             if (ovl.length === 0) {
 31                 body.append(HTMLS.ovl);
 32             }
 33 
 34             this.set("ovl", jQuery("#J_WinpopBox"));
 35             this.set("mask", jQuery("#J_WinpopMask"));
 36         },
 37         bindEvent: function() {
 38             var _this = this,
 39                 ovl = _this.get("ovl"),
 40                 mask = _this.get("mask");
 41             ovl.on("click", ".J_AltBtn", function(e) {
 42                 _this.hide();
 43             });
 44             ovl.on("click", ".J_CfmTrue", function(e) {
 45                 var cb = _this.get("confirmBack");
 46                 _this.hide();
 47                 cb && cb(true);
 48             });
 49             ovl.on("click", ".J_CfmFalse", function(e) {
 50                 var cb = _this.get("confirmBack");
 51                 _this.hide();
 52                 cb && cb(false);
 53             });
 54             mask.on("click", function(e) {
 55                 _this.hide();
 56             });
 57             jQuery(document).on("keyup", function(e) {
 58                 var kc = e.keyCode,
 59                     cb = _this.get("confirmBack");;
 60                 if (kc === 27) {
 61                     _this.hide();
 62                 } else if (kc === 13) {
 63                     _this.hide();
 64                     if (_this.get("type") === "confirm") {
 65                         cb && cb(true);
 66                     }
 67                 }
 68             });
 69         },
 70         alert: function(str, btnstr) {
 71             var str = typeof str === 'string' ? str : str.toString(),
 72                 ovl = this.get("ovl");
 73             this.set("type", "alert");
 74             ovl.find(".J_WinpopMain").html(str);
 75             if (typeof btnstr == "undefined") {
 76                 ovl.find(".J_WinpopBtns").html(HTMLS.alert);
 77             } else {
 78                 ovl.find(".J_WinpopBtns").html(btnstr);
 79             }
 80             this.show();
 81         },
 82         confirm: function(str, callback) {
 83             var str = typeof str === 'string' ? str : str.toString(),
 84                 ovl = this.get("ovl");
 85             this.set("type", "confirm");
 86             ovl.find(".J_WinpopMain").html(str);
 87             ovl.find(".J_WinpopBtns").html(HTMLS.confirm);
 88             this.set("confirmBack", (callback || function() {}));
 89             this.show();
 90         },
 91         show: function() {
 92             this.get("ovl").show();
 93             this.get("mask").show();
 94         },
 95         hide: function() {
 96             var ovl = this.get("ovl");
 97             ovl.find(".J_WinpopMain").html("");
 98             ovl.find(".J_WinpopBtns").html("");
 99             ovl.hide();
100             this.get("mask").hide();
101         },
102         destory: function() {
103             this.get("ovl").remove();
104             this.get("mask").remove();
105             delete window.alert;
106             delete window.confirm;
107         }
108     };
109 
110     var obj = new Winpop();
111     window.alert = function(str) {
112         obj.alert.call(obj, str);
113     };
114     window.confirm = function(str, cb) {
115         obj.confirm.call(obj, str, cb);
116     };
117 })(window, jQuery);

代码略多,关键做以下几点说明:

  • 笔者偷了懒,使用了jQuery,使用之前请先保证已经引入了jQuery
  • 自定义组件结构最终是追加到body中的,所以在引入以上js之前,请先确保文档已经加载完成
  • 组件添加了按ESC、点遮罩层隐藏组件功能
  • 注意:虽然本例中未用到 destory 方法,但读者朋友可以注意一下该方法中的 delete window.alert 和 delete window.confirm ,这样写的目的是保证在自定义组件销毁后,将Alert、Confirm控件恢复到浏览器内置效果
  • 组件最后如果加上 window.Winpop = Winpop ,就可以将对象全局化供其他类调用了

最后

作为一个前端开发工程师,个人觉得Javascript组件开发是一件很有意思的事情,其中乐趣只有自己亲自动手尝试了才会体会得到。前端组件开发往往需要Javascript、CSS和html相互配合,才能事半功倍,上面提到的Winpop也不例外,这里给大家提供一个 完整的demo压缩包,有兴趣的读者朋友,欢迎传播。

webpack - Feature Flag 功能发布控制

$
0
0

背景

很多时候我们会不小心把本地调试的代码发布掉,造成了线上的代码出现问题。或者说暂时不希望某些正在开发的代码被执行,造成线上显示的不不正常或推迟上线。

说明

实现

webpack.config.js里这样写

var webpack = require('webpack');

module.exports = {
  entry: {
    index: "./app/index.js"
  },
  output: {
    path: './run',
    filename: "index.bundle.js"
  },
  plugins: [
    new webpack.DefinePlugin({
      __DEBUG__: true
    })
  ],
  devtool: "#inline-source-map"
};

配置完成后,我们可以这样写代码

var $ = require('./js/lib/jquery');

__DEBUG__ && console.log($);

在webpack编译后会变成这个样子

var $ = require('./js/lib/jquery');

(true) && console.log($);

发布

这个时候我们就要把 __DEBUG__设为 false了,这样在编译完成后就会变成这个样子。

var $ = require('./js/lib/jquery');

(false) && console.log($);

这样子在执行的时候就永远不会执行调试的代码了,然后在发布压缩的时候会被过滤掉。

var $ = require('./js/lib/jquery');

在大部分的压缩中,因为这句代码绝对不会被执行,因此被当成了废代码直接去除掉了。

优点

  • 是一个硬开关。如果是用js本身维护一个配置对象也可以达成这样的效果,但代码依然会跑到线上。使用本方法能强制的把代码滤掉,完全的避免资源浪费。
  • 代码会更加有条理,功能相关的部分会有规律的聚集到一起。
  • 代码上线可以更灵活,不必因为代码没有完全实现而推迟上线,没有完成的功能关闭即可。
  • 灵活下线。线上如果有BUG,立马关闭功能。我感觉这种方法比代码版本回滚要好得多,因为BUG可能不是上个版本产生的。

缺点

  • 环境须用webpack,当然其他的工具可能也可以做到。
  • 工程复杂度增加,成员要严格的做flag条件设置。

扩展

可以做一个功能清单,这样就有了实际的意义了。

new webpack.DefinePlugin({
  __DEBUG__      : true,
  __F_EDITOR__   : true,
  __F_TREE_LIST__: false,
  __F_SIGN_UP__  : true
})

这样就能像做开关一样自由的开启功能点。如果设置的功能点过多,那么最好用单独的一个文件保存。

结语

真实情况中会相当的复杂,如何定义还请自行根据经验判断。如有疑问和纠正可以留言。

Viewing all 148 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>