抓取网页数据的一些方法总结

前段时间PM那边有个需求,需要抓取网页中的一些数据,做完该需求后,发现大概有以下几种方法可以作为参考。

  1. phantomjs
  2. nodejs 配合cheerio模块
  3. php配合file_get_contents()方法

1. phantomjs

phantomjs功能十分强大,Phantom JS是一个服务器端的 JavaScript API 的 WebKit。其支持各种Web标准: DOM 处理, CSS 选择器, JSON, Canvas, 和 SVG。

在我的需求中,phantomjs用于抓取数据只是很少的一个功能,还可以用于网页截图,各种网络请求模拟,模拟用户做网页操作测试,文件输入输出处理等功能。回到需求,要抓取指定网页的数据,大致过程如下:

  1. 引用webpage模块
  2. 调用webpage模块的open方法,打开一个网页
  3. 调用webpage模块的evaluate方法,操作打开页面并返回目标数据
  4. 引用文件系统,将获取的数据保存到本地文件

当然在调用evaluate方法时,还可以动态引用jquery库,更加方便地利用jquery的API操作打开页面的DOM元素,返回目标数据。基本示例如下:

//引入webpage及fs模块
page = require( 'webpage' ).create();
fs = require( 'fs' );

page.open( url, function( status ) {
    if ( status === "success" ) {
        //动态加载jquery,方便dom操作
        page.includeJs( 'jquery', function() {
            //调用evaluate方法,返回目标数据
            var data = page.evaluate( function() {
                var book = {},
                    title = $('#name h2').text(),
                    url = location.href;

                return {
                    title: title,
                    url: url
                }
            } );

            //写入文件
            fs.write( './data.txt', data, 'w');

            page.close();
            page.release();
        } );
    }
} );

从示例代码中可以看到利用phantomjs非常简单,但是在实际使用中,由于抓取的数据量比较大,phantomjs的抓取效率并不高,打开一个页面的时间大概要1s左右,对于十几万的数据量,这种效率是没有办法接受的,为什么会这么慢呢?原因是webpage模块的open方法打开一个页面时,相当于直接用浏览器打开了一个页面,不仅要加载所有的外部资源,还在渲染并执行这行资源,所以抓取大数据量时这种方法并不理想,抓取小数据量还是很不错的选择。

2. nodejs 配合cheerio模块

第二种方法和phantomjs实现原理基本一样,不同的是打开页面方式不一样,这里是直接利用nodejs的http模块,直接利用get方法请求一个页面,所以无须额外加载,渲染并执行外部资源,所以效率相比phantomjs要高很多,特别适合于抓取大量的数据。

在使用之前,先引入cheerio模块,cheerio模块的主要目的将获取的网页源代码转化为dom元素,方便利用dom操作来提取页面中的数据,基本示例如下:

var http = require("http"),
    cheerio = require( "cheerio" );
 
function download( url, callback ) {
  http.get (url, function( res ) {
    var data = "";
    res.on( 'data', function (chunk) {
      data += chunk;
    } );
    res.on( "end", function() {
      callback( data );
    } );
  } ).on("error", function() {
    callback( null );
  } );
}

download( "http://www.baidu.com", function( data ) {
  if (data) { 
    var $ = cheerio.load( data );
    $( "div.artSplitter > img.blkBorder" ).each( function( i, e ) {
        console.log( $( e ).attr( "src" ) );
    } );
    console.log("done");
  } else {
    console.log("error");
  }
} );

3. php的file_get_contents()方法

利用后端php的file_get_contents方法,先获取网页的源代码,然后利用正则匹配出目标数据,当然对于前端工程师来说,利用自己相对熟悉的nodejs来说当然是首选,而且利用dom操作相对于正则匹配要方便很多。

总之,大家可以根据自己的实际需要,选择其中的一种方法,如果有更好的方法,也欢迎大家提出交流!

事件处理函数如何传参问题

工作中,突然一同事问我匿名函数能否解除事件绑定,答案大家都很清楚,肯定是不行的!如果要解除事件绑定,事件处理函数必须赋值给一个变量或对象的一个属性,如下:

var btn = document.getElementById( 'btn-demo' );

//绑定事件
btn.addEventListener( 'click', clickHandle, false );

//解除绑定
btn.removeEventListener( 'click', clickHandle, false );

function clickHandle( event ) {
	//code here
}

然后又追问了一个问题,他说他的使用场景下,事件处理函数需要传入除事件对象以外的参数,如果这样的话,参数没法传过去,突然觉得很熟悉的问题,记得自己曾经也两样的这样的疑问。

其实公司的框架中已经封装了这种情况的方法,几乎所有的框架都会提供这样的方法!如jQuery的$.proxy(),这样一说他说明白了。ES5中已经提供了原生的支持bind方法,现有的高级浏览器都也支持该方法,这里可以针对不支持该方法的浏览器作兼容性处理,如下:

if ( !Function.prototype.bind ) {
	Function.prototype.bind = function( thisObj ) {
		var me = this,
		    slice = Array.prototype.slice,

		    //由于arguments并不是真正的数组,这里借用数组的slice方法取出除函数运行时
		    //改变this对象的thisObj参数外的其余参数
		    args = slice.call( arguments, 1 );

		return function() {
			//合入函数运行时的参数
			me.call( thisObj, args.concat( slice.call( arguments, 0 ) ) );
		}
	}
}

注:bind方法合入了两次参数,第一次是取出函数定义时除改变函数运行时this的thisObj参数外的所有参数,第二次是合并函数运行时的参数。这样我们可以改写上面的事件绑定函数。

<input type="button" id="btn-demo" value="click me" />

<script>
	if ( !Function.prototype.bind ) {
		Function.prototype.bind = function( thisObj ) {
			var me = this,
				slice = Array.prototype.slice,

				//由于arguments并不是真正的数组,这里借用数组的slice方法取出除函数运行时
				//改变this对象的thisObj参数外的其余参数
				args = slice.call( arguments, 1 );

			return function() {
				//合入函数运行时的参数
				me.call( thisObj, args.concat( slice.call( arguments, 0 ) ) );
			}
		}
	}

	( function() {
		var btn = document.getElementById( 'btn-demo' );

		btn.addEventListener( 'click', clickHandle.bind( this, {
			target: btn,
			docId: 'zc1987'
		} ), false );

		function clickHandle( data ) {
			//函数运行时取出event对象
			var event = Array.prototype.slice.call( arguments, -1 );
			console.log( event ); //event对象
			console.log( data ); //额外传入的参数
			console.log( Array.prototype.slice.call( arguments, 0 ) );
		}
	} )();
</script>

注,如果要解除事件绑定的话,需要把事件处理函数赋值给一个变量,最终代码如下:

<input type="button" id="btn-demo" value="click me" />

<script>
	if ( !Function.prototype.bind ) {
		Function.prototype.bind = function( thisObj ) {
			var me = this,
				slice = Array.prototype.slice,

				//由于arguments并不是真正的数组,这里借用数组的slice方法取出除函数运行时
				//改变this对象的thisObj参数外的其余参数
				args = slice.call( arguments, 1 );

			return function() {
				//合入函数运行时的参数
				me.call( thisObj, args.concat( slice.call( arguments, 0 ) ) );
			}
		}
	}

	( function() {
		var btn = document.getElementById( 'btn-demo' ),
			tempHandle = clickHandle.bind( this, {
				target: btn,
				docId: 'zc1987'
			} );

		//绑定事件
		btn.addEventListener( 'click', tempHandle, false );

		//解除绑定
		btn.removeEventListener( 'click', tempHandle, false );

		function clickHandle( data ) {
			//函数运行时取出event对象
			var event = Array.prototype.slice.call( arguments, -1 );
			console.log( event ); //event对象
			console.log( data ); //额外传入的参数
			console.log( Array.prototype.slice.call( arguments, 0 ) );
		}
	} )();
</script>

注:如果在事件处理函数中需要用到event对象,如何拿到呢,事件触发时,默认会传入event对象参数,所以event对象是在所有参数的最后,本例中是通过Array.prototype.slice.call( arguments, -1 )来拿到event对象。

fiddler 常用方法简介

Fiddler是最强大最好用的Web调试工具之一,它能记录所有客户端和服务器的http和https请求,允许你监视,设置断点,甚至修改输入输出数据. 使用Fiddler无论对开发还是测试来说,都有很大的帮助。

Fiddler的基本介绍

Fiddler的官方网站:  www.fiddler2.com

Fiddler官方网站提供了大量的帮助文档和视频教程, 这是学习Fiddler的最好资料。

Fiddler是最强大最好用的Web调试工具之一,它能记录所有客户端和服务器的http和https请求,允许你监视,设置断点,甚至修改输入输出数据,Fiddler包含了一个强大的基于事件脚本的子系统,并且能使用.net语言进行扩展

你对HTTP 协议越了解, 你就能越掌握Fiddler的使用方法. 你越使用Fiddler,就越能帮助你了解HTTP协议.

Fiddler无论对开发人员或者测试人员来说,都是非常有用的工具

Fiddler的工作原理

Fiddler 是以代理web服务器的形式工作的,它使用代理地址:127.0.0.1, 端口:8888. 当Fiddler退出的时候它会自动注销,这样就不会影响别的程序。不过如果Fiddler非正常退出,这时候因为Fiddler没有自动注销,会造成网页无法访问。解决的办法是重新启动下Fiddler.

同类的工具有: httpwatch, firebug, wireshark

QuickExec命令行的使用

Fiddler的左下角有一个命令行工具叫做QuickExec,允许你直接输入命令。

常见得命令有

  • help  打开官方的使用页面介绍,所有的命令都会列出来
  • cls    清屏  (Ctrl+x 也可以清屏)
  • select  选择会话的命令
  • ?.png  用来选择png后缀的图片
  • bpu  截获request

Fiddler中设置断点修改Request

Fiddler最强大的功能莫过于设置断点了,设置好断点后,你可以修改httpRequest 的任何信息包括host, cookie或者表单中的数据。设置断点有两种方法

第一种:打开Fiddler 点击Rules-> Automatic Breakpoint  ->Before Requests(这种方法会中断所有的会话)

如何消除命令呢?  点击Rules-> Automatic Breakpoint  ->Disabled

第二种:  在命令行中输入命令:  bpu www.baidu.com   (这种方法只会中断www.baidu.com)

如何消除命令呢?  在命令行中输入命令 bpu

看个实例,模拟博客园的登录, 在IE中打开博客园的登录页面,输入错误的用户名和密码,用Fiddler中断会话,修改成正确的用户名密码。这样就能成功登录

  1. 用IE 打开博客园的登录界面  http://passport.cnblogs.com/login.aspx
  2. 打开Fiddler,  在命令行中输入bpu http://passport.cnblogs.com/login.aspx
  3. 输入错误的用户名和密码 点击登录
  4. Fiddler 能中断这次会话,选择被中断的会话,点击Inspectors tab下的WebForms tab 修改
  5. 用户名密码,然后点击Run to Completion 如下图所示。
  6. 结果是正确地登录了博客园

Fiddler中设置断点修改Response

当然Fiddler中也能修改Response

第一种:打开Fiddler 点击Rules-> Automatic Breakpoint  ->After Response  (这种方法会中断所有的会话)

如何消除命令呢?  点击Rules-> Automatic Breakpoint  ->Disabled

第二种:  在命令行中输入命令:  bpafter www.baidu.com   (这种方法只会中断www.baidu.com)

如何消除命令呢?  在命令行中输入命令 bpafter,

具体用法和上节差不多,就不多说了。

javascript中自定义事件的模型及简单实现

在js世界里,事件可谓是个大课题,在此讨论的自定义事件也只是冰山一角。

function A() {};
A.prototype = {
    hide: function () {
        console.log('hide');
    }
};
var a = new A(); // 实例化A模块
function B() {};
B.prototype = {
    show: function () 
        console.log('show');
        a.hide(); // 调用A模块隐藏
    }
};
var b = new B();
b.show();

这样代码的问题在于太过于流程化,B模块内置了对A模块的依赖,耦合度高。如果要让B模块独处到另外的页面工作,就会找不到A模块而无法使用。又或者需求变化,B模块显示的时候,同时显示一个新的C模块,那又要去B模块里添加代码,最后B模块只会依赖越来越多,越陷越深,无法自拔啊。

为了让模块间解耦,通常,会把逻辑代码抽离到一个回调函数中,通过配置去指挥代码所需要的处理。

function A() {};
A.prototype = {
    hide: function () {
        console.log('hide');
    }
};
var a = new A();
function B(config) {
    this.onShow = config.onShow;
};
B.prototype = {
    show: function () {
        console.log('show');
        if (this.onShow instanceof Function) {
            this.onShow(); // 调用事件发生后的回调
        }
    }
};
var b = new B({
    onShow: function () {
        a.hide();
    }
});
b.show();

以回调的方式,实现一个原始的类似事件的订阅方式,可以把对外部依赖的代码逻辑通过配置的方式移出模块内,实现上也很简单,可以不依赖于任何机制。

但与此同时,也存在着一定的局限性,比如只能在初始化的时候配置;不能多次添加“监听”;每个需要事件回调的方法都需要添加一个回调事件。

自定义事件

如果以“写分享”这么一个事件来比喻,最开始的那种流程式的方式就好比,分享者每周写了一篇分享,并通过邮件A、B、C等N各人,分享者需要自己维护每个同学的邮件地址。要是有一天,哪个同事离职了,还要从收件人里把他删掉,分享者跟邮件联系人耦合度很高。
而到了采用回调式的方式,就好比分享者发分享时不再需要记得有哪些人,分享者有一个收件人的群组,每次写完就往这个群组里发,不必再关心群组里有哪些人和那些人的去向。由外部去维护这么一个联系人群组。

但对于时刻想偷懒的人来说,这个还不是最方便了,于是,有了一种新的方式,写好的分享,发表到博客中,有兴趣看分享的人去加关注。于是,就有了下面的故事:

// 从前有个叫zc的家伙
var zc = {
	name: 'zhangchen',
	// 他会写分享
	finishShare: function() {
		this.fire('zc_finish_share');
	}
};
 // 使得zc对象具有自定义事件功能。具体实现后面再说明。EventTarget的代码
EventTarget.mixin(zc);
zc.on('zc_finish_share', function() {
	alert(this.name + ':Yeah,写好分享了!');
});
// IT男小a
var a = {
	name: 'IT男小a'
};
// IT男订阅了zc的分享
a.rss = function() {
	zc.on('zc_finish_share', function() {
		alert(a.name + ':顶一个!');
	});
};
a.rss();
// 漂亮mm
var mm = {
	name: '漂亮mm'
};
// 漂亮mm订阅了zc的分享
mm.rss = function() {
	mm.comment = function() {
		alert(mm.name + ':哇,好崇拜啊!');
	};
	zc.on('zc_finish_share', mm.comment);
};
mm.rss();
// zc写好分享了,IT男和漂亮mm都订阅到了
zc.finishShare();

故事描述的正是这种自定义事件的方式。发分享者不再需要关心自己写完分享后还要做什么什么事情,也无需知道看分享的有谁谁谁,只需要认真写好分享就可以了。而订阅分享的人,每期都能看到分享,并做相应的动作。如此一来,分享者和看分享者之间就解耦了。
到这里,故事还没有完

// 漂亮mm觉得zc的分享越来越无聊了
mm.unRss = function() {
    mk.detach('zc_finish_share', mm.comment);
};
mm.unRss();
// mk还是继续写分享,可惜漂亮mm再也看不到了
mk.finishShare();

自定义事件模型

前面啰嗦了那么多,总算了描述了自定义事件的用法,在很多类库中,都添加了很多自定义事件的支持,而自定义事件也不是什么神奇的事情,实现上还是比较简单的。它的实现可以说是前面提到的回调式方式的一个进化版,把回调函数以缓存方式保存起来,在触发的位置把回调函数取出来运行之。

对于自定义事件模型,包含以下:

  • 事件映射的hash表。
  • 创建绑定一个特定事件方法。
  • 触发事件,并调用所有指定的事件处理方法。
  • 删除一个特定事件的方法。

事件映射的hash表

所有绑定的事件,需要有个映射关系的存储,这样才能在事件触发的时候,找到正确的事件并执行它。 缓存数据结构如下:

this: Object
    - _events: Object
        - eventName(eg. show, hide): Array
            - 0: Object
                - fn: Function
                - params: Object
                - scope: Object
            - 1: Object
            - 2: Object
        - otherEventName: Array

创建绑定一个特定事件

创建事件实际上,是把事件和事件的回调缓存起来。

on: function(name, handler, params, scope) {
    this._events = this._events || {};
    this._events[name] = this._events[name] || [];
    this._events[name].push({
        fn: handler,
        params: params || {},
        scope: scope || this
    });
}

触发事件

触发事件时,从缓存列表中找到正确的事件,并执行之。

fire: function(name, args) {
    var arr;
    this._events = this._events || {};
    arr = this._events[name];
    for(var i = 0, len = arr.length; i < len; i++) {
        arr[i].fn.call(arr[i].scope || this, name, args, arr[i].params);
    }
}

解绑事件

解除绑定只需要根据事件名和事件的方法(可选),在缓存列表中找到相应的记录,移出掉即可。

detach: function(name, handler) {
    var arr, new_arr = [];
    this._events = this._events || {};
    arr = this._events[name];
    if (typeof handler === 'function') {
        for (var i = 0, len = arr.length; i < len; i++) {
            if (arr[i] && arr[i].fn !== handler) {
                new_arr.push(arr[i]);
            }
        }
        this._events[name] = new_arr;
    } else {
        delete this._events[name];
    }
}

小结

自定义事件在复杂的面向对象的JS交互里面具有重要的应用价值,一方面可以封装逻辑简化代码,减少代码间的耦合,它的模块更内聚,更容易复用,在业务不可预知的前提下,业务代码改变得更少。而且代码逻辑可以更加符合现实世界的思维,一个个事件发布出来,针对这些事件作出响应,这就是一个业务场景,每个步骤清晰自然,事件发布者和事件订阅者关系也相对独立。

是的,这就是自定义事件,用好这种模式,可以写出更好维护的js代码!

再看javascript正则表达式

创建正则表达式对象

1. 直接量语法

/pattern/attributes

2. 通过new新建一个RegExp对象

new RegExp(pattern, attributes);

参数说明:

  • i — 执行对大小写不敏感的匹配
  • g — 执行全局匹配(查找所有匹配而非在找到第一个匹配后停止)
  • m — 执行多行匹配

常用的元字符:

  • . 查找单个字符,除了换行和行结束符;
  • \w 匹配字母、汉字、数字、下划线等符号;
  • \s 匹配空白符(包含空格、制表符等);
  • \d 匹配数字;
  • \b 匹配位于单词的开头或结尾的匹配;

常用的量词有:

  • ^n 匹配任何开头为 n 的字符串;
  • n$ 匹配任何结尾为 n 的字符串;
  • n+ 匹配任何包含至少一个 n 的字符串;
  • n* 匹配任何包含零个或多个 n 的字符串;
  • n? 匹配任何包含零个或一个 n 的字符串;
  • n{X} 匹配包含 X 个 n 的序列的字符串;
  • n{X, Y} 匹配包含 X 或 Y 个 n 的序列的字符串;

使用场景

  • str.match() — 找到一个或多个正则表达式的匹配
  • str.replace() — 替换与正则表达式匹配的子串
  • str.search() — 检索与正则表达式相匹配的值
  • str.split() — 把字符串分割为字符串数组
  • regExpObject.test() — 检索字符串中指定的值。返回 true 或 false
  • regExpObject.exec() — 检索字符串中指定的值。返回找到的值,并确定其位置
  • regExpObject.compile() — 编译正则表达式

正则表达式中需要转义的字符

^ $ . * + ? = ! : | \ / ( ) [ ] { }

子表达式

先看下面的例子:

var testStr = 'Shpping cost usually would be affected by&nbsp;&nbsp;&nbsp; time';
var result = testStr.match( /&nbsp;{2,}/gi );
console.log( result ); //null

这里发现匹配的结果不是我们期待的,本意是希望把 连续两次或更多次重复出现的找出来。原因是{2, }只作用于紧挨着它的前面一个字符,在这里是分号,所以这个正则只能匹配 ;;;,而不能匹配   

这里就需要用到子表达式,子表达式把一个表达式划分为一系列子表达式的目的是为了把那些表达式当作一个独立的元素来使用。使用子表达式必须要用括号()括起来。

注意:(和)是元字符,如果需要匹配(和)本身,就必须要对其进行转义,\(和\)

我们对之前的正则用子表达式就可以得到我们期待的结果。如下代码所示:

var testStr = 'Shpping cost usually would be affected by&nbsp;&nbsp;&nbsp; time';
var result = testStr.match( /(&nbsp;){2,}/gi );
console.log( result ); //["&nbsp;&nbsp;&nbsp;"] 

说明:子表达式也支持多重嵌套使用。

回溯引用:前后一致匹配

先看以下示例,在web开发中,我们经常需要去匹配HTML标签,大多数的HTML标签都有一个开始标记和结束标记如<h1></h1>,如果只需单纯的匹配h1和div我们可以很容易的构造出该正则表达式:

var html = '<h1>回溯引用匹配示例</h1>',
	regH1 = /<h1>.*<\/h1>/gi;
	
var result = html.match( regH1 );
console.log( result ); //["<h1>回溯引用匹配示例</h1>"] 

注意:以上例子中要注意一个过度匹配的问题,如我们把以上示例改成:

var html = '<h1>回溯引用匹配</h1><h1>这里如果不用"懒惰型",匹配的结果会有问题</h1>',
	regH1 = /<h1>.*<\/h1>/gi;
	
var result = html.match( regH1 );
console.log( result ); //["<h1>回溯引用匹配</h1><h1>这里如果不用"懒惰型",匹配的结果会有问题</h1>"] 

这里并没有按照我们所期待的那样去单独匹配每个h1标签,原因是*是“贪婪型”元字符,它们在进行匹配时的行为模式是多多益善而不是适可而止,它们会尽可能地从一段文字的开头一直匹配到这段文字的结尾,而不是从这段文字的开头匹配到第一个匹配时为止。

在不需要这种“贪婪型”匹配时,我们就需要使用这些元字符的“懒惰型”匹配,“懒惰型”元字符的写法就是在贪婪型元字符后面加一个?即可。如以下贪婪型元字符:

  • * — *?
  • + — +?
  • {n,} — {n,}?

所以我们将以上示例改成懒惰型就可以达到我们想到的目的。如以下代码示例:

var html = '<h1>回溯引用匹配</h1><h1>这里如果不用"懒惰型",匹配的结果会有问题</h1>',
	regH1 = /<h1>.*?<\/h1>/gi;
	
var result = html.match( regH1 );
console.log( result ); //["<h1>回溯引用匹配</h1>", "<h1>这里如果不用"懒惰型",匹配的结果会有问题</h1>"] 

但是以上只能匹配h1标签,如果要匹配所有的html标签呢?我们可以把以上示例改为如下:

var html = '<h1>h1 tag</h1> <h2>h2 tag</h2> <div>div tag</div>',
	regTag = /<\w+\d?>.*?<\/\w+\d?>/gi;
	
var result = html.match( regTag );
console.log( result ); //["<h1>h1 tag</h1>", "<h2>h2 tag</h2>", "<div>div tag</div>"] 

但是以上匹配还是有个问题,它只能保证匹配的内容是html标签,但是不能保证前后的标签一致的匹配,如下代码示例:

var html = '<h1>h1 tag</h1> <h2>h2 tag</h3> <div>div tag</div>',
	regTag = /<\w+\d?>.*?<\/\w+\d?>/gi;
	
var result = html.match( regTag );
console.log( result ); //["<h1>h1 tag</h1>", "<h2>h2 tag</h3>", "<div>div tag</div>"] 

在以上示例中,第二个html标签是以h2开头,h3结尾,这个肯定是不合法的,但是却把它匹配出来了,这显示不是我们想要的结果。解决的办法就必须要用到回溯引用。

回溯引用允许正则表达式模式引用前面的匹配结果,具体到上面的例子,就是匹配html中前面出现的html标签。如下代码示例:

var html = '<h1>h1 tag</h1> <h2>h2 tag</h3> <div>div tag</div>',
	regTag = /<(\w+\d?)>.*?<\/\1>/gi;
	
var result = html.match( regTag );
console.log( result ); //["<h1>h1 tag</h1>", "<div>div tag</div>"] 

说明:以上示例,匹配的结果中已经把不合法的匹配去除掉了。注意此时\w+\d?放在子表达式中,后面的\1正是一个回溯引用,它引用的正是前面划分出来的子表达式,当\w+\d?匹配到h1时,\1也会匹配到h1。

\1代表模式中第一个子表达式,\2代码第二个子表达式,依此类推,其实可以回溯引用想象成一个变量。

注意:子表达式是通过它们的相对位置引用的,但是这种模式存在一个不足之处,如果子表达式的位置发生变化或删除添加子表达式,整个模式也就不能按照原来的方式工作。

回溯引用在查找替换中的应用,看下面的示例:

var testStr = 'zhangchen2397@gmail.com',
	regEmail = /(\w+[\w\.]*@[\w\.]+\.\w+)/gi;

var result = testStr.replace( regEmail, '<a mailto="$1">$1</a>' );
console.log( result ); //<a mailto="zhangchen2397@gmail.com">zhangchen2397@gmail.com</a> 

我们将匹配邮箱的正则放在()中,然后在replqce方法中用$1引用子表达式中匹配的内容。这样在进行批量替换时非常方便,一般的文本编辑器都支持这种匹配方式。

前后查找 ?<= 及 ?=

前面我们用回溯引用查找出了html中所有的标签的内容,如果我们只想匹配标签内的内容,不匹配标签本身呢?这里就需要用到向前及向后查找,先看向前查找,向前查找是以?=,需要匹配的文本跟在=后面,如下代码示例:

var hTab = '<h1>alibaba.com</h1>',
	regH = /<h1>.*?(?=<\/h1>)/gi;

var result = hTab.match( regH );
console.log( result ); //["<h1>alibaba.com"] 

从以上匹配的结果来看,达到了我们的期望,注意,在向前查找里,被匹配的文本不包含在最终返回的匹配结果里。有了向前查找,我们就可以再利用向后查找( ?<= ),达到我们最终的目的,但是很遗憾,javascript目前还不支持向后查找。如果支持的话,我们可以综合利用向前及向后查找达到匹配标签内内容的目的,如下代码所示:

var hTab = '<h1>alibaba.com</h1>',
	regH = /(?<=<h1>).*?(?=<\/h1>)/gi;

var result = hTab.match( regH );
console.log( result );

条件匹配

还是通过一个例子来说明什么是条件匹配,如我们需要匹配页面中的所有<img>标签,不仅如此,如果这个<img>标签有链接,这个链接标签也要完整的匹配出来。这样匹配时就会有一个前置条件,如果有<a>标签,后面才会去匹配</a>,所以这里就需要用到条件匹配。

条件匹配的语法是:?(backreference)true-regex|false-regex,其中?表示这是一个条件,括号中的backreference是一个回溯引用,true-regex是只在backreference存在时执行的子表达式,false-regex表示backreference不存在时执行的子表达式。如以下代码喜示例

var imgList = '<img src="001.jpg" /> <a href="#"><img src="002.jpg" /></a>',
	regImg = /(<a\s+[^>]+>)?<img\s+[^>]+ \/>(?(1)<\/a>)/gi;

var result = imgList.match( regImg );

很遗憾,javascript并不支持条件匹配,执行时脚本会报错。

关于JSONP跨域请求数据的原理及最优解决方案

上篇文章中提到利用JSONP请求数据时在IE9下的一个安全策略的问题,这里我们来详细的再次讲解JSONP请求数据的原理、优化方案及安全问题。

什么是JSONP?它的实现原理?

JSONP即JSON with Padding。由于同源策略的限制,XmlHttpRequest只允许请求当前源(域名、协议、端口)的资源。如果要进行跨域请求,我们可以通过使用 html的script标记来进行跨域请求,并在响应中返回要执行的script代码,其中可以直接使用JSON传递javascript对象。这种跨域的通讯方式称为JSONP。

其实说白了,就是利用script标签这种不受跨域的限制向服务器发送一个请求,服务器端返回的为一个可执行的javascript文件,如下代码示例:

<script src="response_jsonp.html?user_id=1230"></script>
<div id="data"></div>

<script>
	( function( doc ) {
		var data = doc.getElementById( 'data' );
		data.innerHTML = 'id: ' + jsonp.id + '<br/>name: ' +jsonp.name;
	} )( document );
</script>

首先向服务器端发起一个请求并传入参数user_id,服务器端返回的数据如下:

window[ "jsonp" ] = {
	"id": "12",
	"name": "jsonp"
}

优化方案

服务器端返回的JSON格式的数据直接赋值给window下的jsonp变量,这样在页面中就可以通过jsonp这个变量取得数据了。

不过上面的代码看起来并不优雅,我们把script标签直接写在了请求的页面中,这样存在一个风险,如果我们在没有拿到数据前就用了jsonp这个变量,js就会报错,阻塞后面脚本的执行。

之前一篇中提到过无阻塞式加载js,我们可以通过动态创建script标签,异步向服务器端发送请求,代码如下:

<div id="data"></div>

<script>
	function loadScript( url, callback ) {
		var script = document.createElement( "script" );
		script.type = "text/javascript";

		if ( script.readyState ) {
			script.onreadystatechange = function() {
				if ( script.readyState == "loaded" || script.readyState == "complete" ) {
					script.onreadystatechange = null;
					if ( callback ) {
						callback();
					}
				}
			}
		} else {
			script.onload = function() {
				if ( callback ) {
					callback();
				}
			}
		}

		script.src = url;
		document.getElementsByTagName( "head" )[ 0 ].appendChild( script );
	}
</script>

<script>
	( function( doc ) {
		loadScript( 'response_jsonp.html?user_id=1230', function() {
			var data = doc.getElementById( 'data' );
			data.innerHTML = 'id: ' + jsonp.id + '<br/>name: ' +jsonp.name;
		} );
	} )( document );
</script>

我们把动态创建script标签的方法封装在loadScript()方法中,接受两个参数,一个是请求的url,另外一个是请求成功后的回调函数,这样就可以保证我们执行的代码是在请求成功之后执行的,这样就不会造成先执行,但还没有请求成功的问题。

但是我们还发现一个问题,服务器端返回的JSON格式的数据是直接赋值给了window下的全局变量,这样对全局造成了污染,并不好。更好的解决办法是在请求的url后加一个回调函数的参数,如下代码所示:

<div id="data"></div>

<script>
	function loadScript( url, callback ) {
		var script = document.createElement( "script" );
		script.type = "text/javascript";

		if ( script.readyState ) {
			script.onreadystatechange = function() {
				if ( script.readyState == "loaded" || script.readyState == "complete" ) {
					script.onreadystatechange = null;
					if ( callback ) {
						callback();
					}
				}
			}
		} else {
			script.onload = function() {
				if ( callback ) {
					callback();
				}
			}
		}

		script.src = url;
		document.getElementsByTagName( "head" )[ 0 ].appendChild( script );
	}
</script>

<script>
	( function( doc ) {
		window[ 'jsonParse' ] = function( data ) {
			var data = doc.getElementById( 'data' );
			data.innerHTML = 'id: ' + data.id + '<br/>name: ' +data.name;
		};

		loadScript( 'response_jsonp.html?user_id=1230&callback=jsonParse' );
	} )( document );
</script>

此时服务器端返回的数据为,如后端语言为php,则返回的代码为:

echo $_GET["callback"]."( '{ "id": "12", "name": "jsonp"}' )"

这样就可以解决变量全局污染的问题,同时也让我们的接口变得更加通用,而不是写死的。

JSONP的不足之处及安全问题

JSONP的好处就是能够很好的解决跨域请求的问题,但是也有一些不足之处:

  • 请求的状态不够丰富,我们只能侦听请求成功时的状态,如果请求失败,我们无法通过一个状态码知晓,所以如果请求失败的话,没有任何反馈给用户,可能会造成用户的困惑。
  • 由于script标签不受同源的限制,同时也造成了一些安全性的问题
    使用远端网站的 script 标签会让远端网站得以注入任何的内容至网站里。如果远端的网站有 JavaScript 注入漏洞,原来的网站也会受到影响。
    现在有一个正在进行计划在定义所谓的 JSON-P 严格安全子集,使浏览器可以对 MIME 类别是“application/json-p”请求做强制处理。如果回应不能被解析为严格的 JSON-P,浏览器可以丢出一个错误或忽略整个回应。

通过动态添加script标签请求数据(JSONP)的一些问题

在做获取订单数量的需求中,由于存在跨域的问题,最终采用YAHOO.util.Get.script()方式请求数据,在本地和工程师测试联调时都一切正常,没有出现什么问题。后端返回的的值为

window[ 'safepayOrderCount' ] = {
	"targetId": 489100971,
	"totalNum": 600
}

在预发布验证时发现在IE9下有js报错,也就是后端返回的’safepayOrderCount’变量没有定义,其它浏览器下都正常。

找了半天原因不知所措,明明都已经正确返回了这段js,为什么还报变量没有定义的错误。后来用Fiddler调试查看请求,最终定位到是服务器端返回的头部信息所致。

其中返回的头部信息中包含以下字段,正是因为这个字段导致了以上问题。

X-Content-Type-Options: nosniff

直接引用官方说明

impacts the browser’s behavior when the server sends the X-Content-Type-Options: nosniff header on its responses. If the nosniff directive is received on a response retrieved by a SCRIPT reference, IE will not load the “script” file if the MIME type does not match one of the following values ["text/javascript", "application/javascript", "text/ecmascript", "application/ecmascript", "text/x-javascript", "application/x-javascript", "text/jscript", "text/vbscript", "text/vbs"].

也就是说当从服务端返回的头部信息中设置了X-Content-Type-Options: nosniff,且请求的url是通过script标签引用的方式,如果返回的Content-Type类型不是以下之一["text/javascript", "application/javascript", "text/ecmascript", "application/ecmascript", "text/x-javascript", "application/x-javascript", "text/jscript", "text/vbscript", "text/vbs"]的话,IE9下是不会加载请求的js文件的。
否则的话就会出现下面的警告

SEC7112: Script from http://transaction.alibaba.com/safepay/getTotalNumByProductIdAndType.htm?targetId=489100971&startDate=2012-04-19&endDate=2012-05-31&rnd=1335399108987 was blocked due to mime type mismatch

原因找到了,解决办法

  1. 服务端返回的头部信息中去掉X-Content-Type-Options: nosniff
  2. 将服务端返回的头部信息中的Content-Type字段设置了以上提到中的任意一种即可。

此问题的相关参考:

PS: 前面提到后端返回的数据格式为:

window[ 'safepayOrderCount' ] = {
	"targetId": 489100971,
	"totalNum": 600
}

这里我们把返回的数据直接添加到了windows对象下,这样造成了一个全局的污染,好的作法是通过callback来处理这些数据,这些内容将在下文作详细说明!

关于innerHTML、appendChild及relaceChild方法的性能比较

innerHTML方法

<html>
	<head>
		<title>Performance Test</title>
	</head>
	<body>
		<div id="container"></div>
		<script>
			var iCurrentTime = ( new Date() ).getTime(),
				oContainer = document.getElementById( 'container' ),
				aTemp = [];
				
			for( var i=0; i<50000; i++ ) {
				aTemp.push( '<div>' + i +'</div>' );
			}
			oContainer.innerHTML = aTemp.join('');
			aTemp = null;
			try {
				console.log( ( new Date() ).getTime() - iCurrentTime );
			} catch( e ) {
				alert( ( new Date() ).getTime() - iCurrentTime );
			}
		</script>
	</body>
<html>

测试结果:执行速度由快到慢依次为:Chrome > IE > firefox

appendChild方法

<html>
	<head>
		<title>Performance Test</title>
	</head>
	<body>
		<div id="container"></div>
		<script>
			var iCurrentTime = ( new Date() ).getTime(),
				doc = document,
				oContainer = doc.getElementById( 'container' ),
				oDiv = doc.createElement( 'div' ),
				oClonedDiv,
				oDocFragment = doc.createDocumentFragment();
				
			for ( var i=0; i<5000; i++ ) {
				oClonedDiv = oDiv.cloneNode( false );
				oClonedDiv.innerHTML = i;
				oDocFragment.appendChild( oClonedDiv );
			}
			oContainer.appendChild( oDocFragment );
			oDiv = null;
			oClonedDiv = null;
			oDocFragment = null;
			try {
				console.log( ( new Date() ).getTime() - iCurrentTime );
			} catch( e ) {
				alert( ( new Date() ).getTime() - iCurrentTime );
			}
		</script>
	</body>
<html>

测试结果:执行速度由快到慢依次为:Chrome > firefox > IE

replaceChild 方法

<html>
	<head>
		<title>Performance Test</title>
	</head>
	<body>
		<div id="container"></div>
		<script>
			var iCurrentTime = ( new Date() ).getTime(),
				doc = document,
				oContainer = doc.getElementById( 'container' ),
				onewContainer = oContainer.cloneNode(false),
				aTemp = [];
				
			for( var i=0; i<50000; i++ ) {
				aTemp.push('<div>' + i +'</div>');
			}
			onewContainer.innerHTML = aTemp.join( '' );
			doc.body.replaceChild( onewContainer, oContainer );
			try {
				console.log( ( new Date() ).getTime() - iCurrentTime );
			} catch( e ) {
				alert( ( new Date() ).getTime() - iCurrentTime );
			}		
		</script>
	</body>
<html>

测试结果:执行速度由快到慢依次为:firefox> chrome > IE

小结

对于三种不同的方法,同一浏览器执行速度由快到慢表现为:

  • chrome: appendChild > replaceChild > innerHTML
  • firefox: replaceChild > appendChild > innerHTML
  • IE: innerHTML > appendChild > replaceChild

从测试结果来看:不能简单的认为哪种方法优于哪种方法,在不同的浏览器中表现都有差异,不过在高级浏览器中,DOM提供的原生方法有快于innerHTML方法,只有在IE的低版本中innerHTML方法要比DOM原生方法快。

javascript判断变量是否声明或已声明但没赋值的常用方法

工作中经常会根据一个变量是否存在然后再做一些其它的操作,上次在工作中遇到过一些小问题,花点时间去总结了下,大概有以下几种常用的方法:

方法1:

if ( !a ) {
	var a = 'test';
}
console.log( a ); //test

注意:如果把var去掉,如下代码

if ( !a ) {
	a = 'test'; //注意此处没有加var
}
console.log( a ); //Uncaught ReferenceError: b is not defined

如果没有加var, 则会报引用的错误,为什么加了var后就没有问题呢?原因是Javascript解释器的工作方式,它是”先解析,后运行”,方法一的代码实际上可以等价于以下代码:

var a;
if ( !a ) {
	a = 'test';
}
console.log( a );

这是因为var命令具有”代码提升”(hoisting)作用。Javascript解释器只”提升”var命令定义的变量,对不使用var命令、直接赋值的变量不起作用。

方法2:

if ( !window.a ) {
	var a = 'test'; 
}
console.log( a ); //test

这里去掉var,代码也能正常运行,因为这里相当于判断window对象是否有a这个属性,这样可以避免因为没有定义a而导致的错误,但是为了书写规范,定义时都要加上var,不然就造成对全局的污染。

方法3 算是最常用的方法了

if ( typeof a === 'undefined' ) {
	var a = 'test';
}
console.log( a ); //test

方法4:

if ( a === undefined ) {
	var a = 'test';
}
console.log( a );//test

注意这里的undefined不能加引号,这里比较的是undefined的这种数据类型,同样的道理,这里必须加var, 否则就会报错。

方法5:

 if ( !( 'a' in window ) ) {
	window.a = 'test';
 }
 console.log( a ); //test

注意这里如果写成 var a = ‘test’, 结果将是undefined,原因和前面一样,var命令具有”代码提升”(hoisting)作用,所以if内的语句根本就不会去执行。

方法6:

 if ( !window.hasOwnProperty( 'a' ) ) {
	window.a = 'test';
 }
 console.log( a ); //test

这里如果写成var a = ‘test’, 结果将是undefined, 原因还是一样。

PS:判断变量的数据类型

Javascript中有6种不同的数据类型:number、string、boolean、undefined 、object、null,其中函数和数组都属于object类型。

可以用typeof 方法去验证变量中哪种类型,如以下代码:

var a = 0,
	b = 'test',
	c = true,
	d = [ 1, 2, 3 ],
	e = function() {},
	f = undefined,
	g = null,
	h = {};

console.log( typeof a ); //number
console.log( typeof  b );//string
console.log( typeof  c );//boolean
console.log( typeof  d );//object
console.log( typeof  e );//function
console.log( typeof  f );//undefined
console.log( typeof  g );//object
console.log( typeof  h );//object

可以看到,typeof 方法不能精确地判断数组类型,此时可以用instanceof方法判断

var a = [ 1, 2, 3 ];
console.log( a instanceof Array ) //true

注意:由于没有声明的变量和已声明但没有赋值的变量的typeof的类型都是undefined,所以不能根据变量的类型为undefined判断其没有声明。

一般的做法是,声明变量时,都给出默认值,这样就可以判断当变量类型为undefined时,其一定是没有声明。

有时我们还经常会去判断一个变量是否为空,这里要区分几种情况,第一种情况是去判断一个字符串或数组类型的变量,这种情况很容易去判断,如以下代码:

var a = '',
	b = [];

if ( a === '' ) {
	//code here
}
//或
if ( !a ) {
	//code here
}

if ( b === '' ) {
	//code here
}
//但这里不能
if ( !b ) {
	//code here
}

还有一种情况是判断一个自定义对象是否为空,当然,我们知道一个自对象永远不可能是空对象,因为它们都会默认继承于Object对象,这里判断是否为空是指有没有自己的属性或方法,如以下判断方法:

function isEmptyObject( object ) {
	var i = 0;
	for ( var k in object ) {
		if ( object.hasOwnProperty( k ) ) {
			i++;
		}
	}
	if ( !i ) {
		return true;
	} else {
		return false;
	}
}
var b = { a: '1', c: '2' };
console.log( isEmptyObject( b ) ); //false
console.log( isEmptyObject( {} ) ); //true

在IE浏览器下对select下拉框通过innerHTML添加option选项无效

最近在工作中做一个二级类目联动中,需要动态给select下拉框添加option选项,一开始直接用innerHTML方式添加,但是在IE浏览器下发现有问题,查了原因发现IE下确实不支持这种写法。如下代码所示:

<select name="category" id="J-category"></select>

<script>
	( function() {
		var cateSelect = document.getElementById( 'J-category' );
		cateSelect.innerHTML = '<option value="Apparel">Apparel</option>';
		try {
			console.log( cateSelect.innerHTML );
			//<option name="Apparel" value="Apparel">Apparel</option>
		} catch( e ) {
			alert( cateSelect.innerHTML );
			//Apparel</OPTION>
		}
	} )();
</script>

IE下很奇怪的把第一个option选项的前部分截掉了。

动态添加option选项的解决方法:

1. DOM方法,如下代码所示:

<select name="category" id="J-category"></select>

<script>
	( function() {
		var cateSelect = document.getElementById( 'J-category' ),
			newOption = document.createElement( "option" ),
			newOptionText = document.createTextNode( "Apparel" );

		newOption.appendChild( newOptionText );
		newOption.setAttribute( "value", "Apparel" );
		cateSelect.appendChild( newOption );
	} )();
</script>

2. 通过option的构造函数的创建option选项,如下代码所示:

<select name="category" id="J-category"></select>

<script>
	( function() {
		var cateSelect = document.getElementById( 'J-category' ),
			newOption = new Option( "Apparel", "Apparel" );

		cateSelect.options[ 0 ] = newOption;
	} )();
</script>

说明:先实例出一个option元素,第一个参数为option的text值, 第二个参数为option的value值,然后将新创建的option添加到下拉框的options集合中来。

3. 通过下拉框的add()方法,如下代码所示:

<select name="category" id="J-category"></select>

<script>
	( function() {
		var cateSelect = document.getElementById( 'J-category' ),
			newOption = new Option( "Apparel", "Apparel" );

		cateSelect.add( newOption, undefined );
	} )();
</script>

说明:add()方法接受两个参数,第一参数为要添加的新选项,第二参数为将位于新选项之后的选项。如想新添加的option选项添加到最后一个选项,需将第二参数设置为null。在IE中,第二参数是可选的,而且如果指定,第二参数必须是新option选项之后的索引;在兼容DOM的浏览器中必须要给定第二个参数,为了兼容处理,为第二参数传入undefined。