cy0707 / learn_javascript Goto Github PK
View Code? Open in Web Editor NEWjavascript的各种知识点的学习和总结,并做了一些小demo
javascript的各种知识点的学习和总结,并做了一些小demo
表示一个正则表达式的开始与结束
例如:
//下面的斜杠就是界定符
var reg = / [0-9] /;
原子可以分为可见原子和不可见原子。
注意:由于在编程中文字有几种不同的编码方式,当对文字进行正则表达式的匹配,建议把文字转为Unicode字符,这样可以保证匹配正确。
要匹配原子,直接输入字符即可。
var reg = / imooc.+123/
//上面这个表示imooc加上除换行符之外任意字符至少出现一次加上123
var str = " imooc-123123124322123"
//对于贪婪匹配结果是 imooc-123123124322123 这个最长的
//对于懒惰匹配结果是 imooc-123 这个最短的
如果未指定修正模式的话,其默认为贪婪模式
常见的修正模式,有以下几种
在慕课网学习,利用原生js和jquery以及CSS3来进行瀑布流的布局,
大家有兴趣可以取看看。下面我想对这门课程所学到的东西做一个总结。源码我已经发在这个仓库里卖waterfall文件夹里。
这个布局就类似与花瓣网站一样,在每一列中是等宽不等高的。当我们滑到页面的最底部的时候,可以无线加载。
利用的是定位的原理,包裹所有元素的大盒子,是相对定位的。假如第一排,有六个元素,他们的宽度是一样,但是他们高度却不是一样的,下一排的第一个,要以第一排的高度最小的进行定位,然后更新那六个的高度,下一个元素再以更新后最小高度进行定位。如此循环。
当最后一个元素的的高度露出一般,就开始加载数据,实现无线加载的原理,即当最后一个元素里它relative元素的offsetTop+自身高度的一半小于浏览器可视高度加上scrollTop的,即可以加载数据了。
最后一排的有些元素,只有一小半,后面的跑到最顶端上面去了,这是出现的问题。
这个上网搜了一下解决方法是:在大盒子包裹的小盒子里面加上以下css就可以解决
-webkit-column-break-inside: avoid; /* Chrome, Safari, Opera */
page-break-inside: avoid; /* Firefox */
break-inside: avoid; /* IE 10+ */
RL.createObjectURL()方法会根据传入的参数创建一个指向该参数对象的URL. 这个URL的生命仅存在于它被创建的这个文档里. 新的对象URL指向执行的File对象或者是Blob对象.
var url = URL.createObjectURL(File || Blob);
File对象,就是一个文件,比如用input type="file"
标签来上传文件,那么里面的每个文件都是一个File对象.
Blob对象,就是二进制数据,比如通过new Blob()创建的对象就是Blob对象.又比如,在XMLHttpRequest里,如果指定responseType为blob,那么得到的返回值也是一个blob对象.
每次调用createObjectURL的时候,一个新的URL对象就被创建了.即使你已经为同一个文件创建过一个URL. 如果你不再需要这个对象,要释放它,需要使用URL.revokeObjectURL()方法. 当页面被关闭,浏览器会自动释放它,但是为了最佳性能和内存使用,当确保不再用得到它的时候,就应该释放它
URL.revokeObjectURL()方法会释放一个通过URL.createObjectURL()创建的对象URL.
当你要已经用过了这个对象URL,然后要让浏览器知道这个URL已经不再需要指向对应的文件的时候,就需要调用这个方法.
具体的意思就是说,一个对象URL,使用这个url是可以访问到指定的文件的,但是我可能只需要访问一次,一旦已经访问到了,这个对象URL就不再需要了,就被释放掉,被释放掉以后,这个对象URL就不再指向指定的文件了.
比如一张图片,我创建了一个对象URL,然后通过这个对象URL,我页面里加载了这张图.既然已经被加载,并且不需要再次加载这张图,那我就把这个对象URL释放,然后这个URL就不再指向这张图了.
<input type="file" id="fileElem" multiple accept="image/*" style="display:none" onchange="handleFiles(this.files)">
<a href="#" id="fileSelect">Select some files</a>
<div id="fileList">
<p>No files selected!</p>
</div>
<script>
window.URL = window.URL || window.webkitURL;
var fileSelect = document.getElementById("fileSelect"),
fileElem = document.getElementById("fileElem"),
fileList = document.getElementById("fileList");
fileSelect.addEventListener("click", function (e) {
//如果文件存在的话
if (fileElem) {
fileElem.click();
}
e.preventDefault(); // prevent navigation to "#"
}, false);
function handleFiles(files) {
if (!files.length) {
// 如果没有选择文件,那么就提示没有选择文件
fileList.innerHTML = "<p>No files selected!</p>";
} else {
// 选择文件的话,先把提示的文件清空
fileList.innerHTML = "";
var list = document.createElement("ul");
fileList.appendChild(list);
for (var i = 0; i < files.length; i++) {
// 文件的个数有几个,创建几个li
var li = document.createElement("li");
list.appendChild(li);
var img = document.createElement("img");
// 获取这个图片的url
img.src = window.URL.createObjectURL(files[i]);
img.height = 60;
img.onload = function() {
// 当图片文件加载完成,释放这个url
window.URL.revokeObjectURL(this.src);
}
li.appendChild(img);
var info = document.createElement("span");
info.innerHTML = files[i].name + ": " + files[i].size + " bytes";
li.appendChild(info);
}
}
}
</script>
window.URL.revokeObjectURL(objectURL);
从前,input:file是不能主动触发click事件的,据说是安全方面的考虑。因此,在美化input:file的时候就麻烦了,最通用的方案是,制作一个经过美化后的,然后把input:file调整好尺寸,覆盖在<button>
上方,然后再设置 opacity:0;
,如此一来,用户看到的是<button>
,点击的却其实是input:file
。
fileSelect.addEventListener("click", function (e) {
//点击a标签,如果input的type为file的话,那么就触发input的点击事件
//这样无论input的位置在哪里,都可以触发
if (fileElem) {
fileElem.click();
}
e.preventDefault(); // prevent navigation to "#"
}, false);
一个例子
URL.createObjectURL和URL.revokeObjectURL
html5允许在click事件的callback中主动出发input:file的click事件
最简单的去重方法, 实现思路:新建一新数组,遍历传入数组,值不在新数组就加入该新数组中;注意点:判断值是否在数组的方法“indexOf”是ECMAScript5 方法,IE8以下不支持,需多写一些兼容低版本浏览器代码,源码如下:
// 最简单数组去重法
function unique1(array){
var n = []; //一个新的临时数组
//遍历当前数组
for(var i = 0; i < array.length; i++){
//如果当前数组的第i已经保存进了临时数组,那么跳过,
//否则把当前项push到临时数组里面
if (n.indexOf(array[i]) == -1) n.push(array[i]);
}
return n;
}
// 判断浏览器是否支持indexOf ,indexOf 为ecmaScript5新方法 IE8以下(包括IE8, IE8只支持部分ecma5)不支持
if (!Array.prototype.indexOf){
// 新增indexOf方法
Array.prototype.indexOf = function(item){
var result = -1, a_item = null;
if (this.length == 0){
return result;
}
for(var i = 0, len = this.length; i < len; i++){
a_item = this[i];
if (a_item === item){
result = i;
break;
}
}
return result;
}
}
该方法执行的速度比其他任何方法都快, 就是占用的内存大一些;实现思路:新建一js对象以及新数组,遍历传入数组时,判断值是否为js对象的键,不是的话给对象新增该键并放入新数组。注意点: 判断是否为js对象键时,会自动对传入的键执行“toString()”,不同的键可能会被误认为一样;例如: a[1]、a["1"] 。解决上述问题还是得调用“indexOf”。
// 速度最快, 占空间最多(空间换时间)
function unique2(array){
var n = {}, r = [], len = array.length, val, type;
for (var i = 0; i < array.length; i++) {
val = array[i];
type = typeof val;
if (!n[val]) {
n[val] = [type];
r.push(val);
} else if (n[val].indexOf(type) < 0) {
n[val].push(type);
r.push(val);
}
}
return r;
}
还是得调用“indexOf”性能跟方法1差不多,实现思路:如果当前数组的第i项在当前数组中第一次出现的位置不是i,那么表示第i项是重复的,忽略掉。否则存入结果数组
function unique3(array){
var n = [array[0]]; //结果数组
//从第二项开始遍历
for(var i = 1; i < array.length; i++) {
//如果当前数组的第i项在当前数组中第一次出现的位置不是i,
//那么表示第i项是重复的,忽略掉。否则存入结果数组
if (array.indexOf(array[i]) == i) n.push(array[i]);
}
return n;
}
虽然原生数组的”sort”方法排序结果不怎么靠谱,但在不注重顺序的去重里该缺点毫无影响。实现思路:给传入数组排序,排序后相同值相邻,然后遍历时新数组只加入不与前一值重复的值。
// 将相同的值相邻,然后遍历去除重复值
function unique4(array){
array.sort();
var re=[array[0]];
for(var i = 1; i < array.length; i++){
if( array[i] !== re[re.length-1])
{
re.push(array[i]);
}
}
return re;
}
源自外国博文,该方法的实现代码相当酷炫;实现思路:获取没重复的最右一值放入新数组。(检测到有重复值时终止当前循环同时进入顶层循环的下一轮判断)
// 思路:获取没重复的最右一值放入新数组
function unique5(array){
var a = [], l = this.length;
for(var i=0; i<l; i++) {
//里面的for循环,例如第一次循环,i=0;
for(var j=i+1; j<l; j++)
//对数组后面的项进行比较,一旦重复,跳出循环,j重新赋值为,与i相等,直达最后哪一项重复
//他的后面没有重复的项,则被推入数组。想法相当巧妙
if (this[i] === this[j]) j = ++i;
//外面的for循环,没有重复的推入新的数组
a.push(this[i]);
}
return a;
}
文章链接 [http://www.codeceo.com/article/javascript-delete-array.html]
学了这节课,感觉收获了许多,所以想记一下笔记。
第一步:创建一个构造函数CusScrollBar
第二步:通过new操作符实例化这个构造函数(初始化函数_init)
滑块的可移动距离 = 整个滚动条的高度 - 滑块的高度
内容可滚动的高度 = 内容的整个高度 - 内容的高度
内容滚动的高度与滑块滚动的距离这两者的比率是相同的
滑块移动的距离/滑块可移动的距离 == 内容滚动的高度/ 内容可滚动的高度
上面这个式子可以变形:
x1/y1 = x2/y2;
x1/y2 = x2y1;
x1/x2 = y1/y2;
所以比率可以用滑块可移动的距离/内容可滚动的高度 ==滑块移动的距离/内容滚动的高度
具体过程:
鼠标拖动滑块的距离——滑块移动的距离——滑块可移动的距离——内容滚动的高度——内容可滚动的高度——设置滑块位置。
//第一种
(function(){
//do something here;
})();
//第二种
(function(){
//do something here;
}());
//第三种,在开头加上!或者~、-、+等
!function(){
//do something here;
};
JavaScript 文法明确规定表达式语句不得以 function 开头,不然会报错。所以可以使用其他可用的标识符使其成为合法的字符串。
firefox中的滚轮事件DOMMouseScroll,其他浏览器滚轮事件mousewheel
var mouseWheel = document.getElementById('mouseWheel');
if (mouseWheel.addEventListener) {
//firefox
mouseWheel.addEventListener('DOMMouseScroll', function(event) {
//滚轮的事件对象属性
event.target.innerHTML = event.detail;
event.stopPropagation();
event.preventDefault();
}, false);
}
//其他浏览器
mouseWheel.onmousewheel = function(event) {
event = event || window.event;
//滚轮的事件对象属性
mouseWheel.innerHTML = event.wheelDelta;
event.returnValue = false;
}
鼠标滚轮事件属性浏览器的差异
在jquery中,最终传入事件处理程序的 event 其实已经被 jQuery 做过标准化处理,其原有的事件对象则被保存于 event 对象的 originalEvent 属性之中,每个 event 都是 jQuery.Event 的实例。
jQuery.Event.originalEvent
指向原生事件
对与每一个标签对应的相应的内容,当内容不足时,即内容很少,没有可视区域的那么高,那么当点击切换tab的话,那么此时标题的定位不是最上面的。因为scrollTop属性是向上卷起的距离,但是此时这个tab内容不够,那么上一个tab的内容会占据这个tab的内容的一部分。老师的思路是加一个矫正元素,根据内容高度来计算矫正元素的高度。其实有一个简单的办法,而且不需要js控制。那就是使每一个tab的内容设置为最小高度为100%.这样就可以了。
滑块的高度可以根据内容的多少,动态的变化。可以根据这个比率来设置可视区域的大小/整个内容的高度 ==滑块的大小/整个滚动条的大小
html的结构优化
<div class="scroll-cont">
<div class="scroll-ol">
<h3 class="anchor">春天来了</h3>
<p>xxxxx</p>
</div>
<div class="scroll-ol">
<h3 class="anchor">春天来了</h3>
<p>xxxxx</p>
</div>
....
</div>
typeof操作符---是用来检测给定变量的数据类型。
其返回值有以下:
var msg = "hello";
console.log(typeof msg )
//输出的是string
typeof操作符是确定一个变量是字符串,布尔值还是undefined的最佳工具。如果变量是一个对象或者null,
typeof就会返回object。
var n =null;
var o = new Object();
console.log(typeof n);
//object
console.log(typeof o);
//object
typeof在检测基本类型时,是一个不错的选择,但在检测引用类型的值时,这个操作符用处就不大了。
我们想知道是不是某个值是不是对象,而是什么类型的对象。此时我们就需要instanceof操作符。
其语法是: result = variable instanceof constructor
如果变量是给定引用类型的实例,就会就会返回true。
console.log(person instanceof Object)
//person 是对象吗。
console.log(person instanceof Array)
//person 是数组吗。
函数定义有两种方式:一是函数的声明,二是函数的表达式。
//函数的声明
function functionName(arg0, arg1, arg2) {
//函数体
}
//函数的表达式
var functionName = function() {
//函数体
}
对于函数的声明:有一个重要的特征那就是函数声明提升,其意思是在执行代码之前会读取函数的声明,这就意味着可以把函数声明放在调用它的语句的后面。
//函数的调用
sayHi();
//输出的是hello
function sayHi(){
console.log("hello");
}
函数的表达式:这种形式看起来好像是变量赋值语句,即创建一个函数并将它赋值给变量functionName,在这种情况下创建的函数,叫匿名函数。因为function关键字后面没有标识符。函数表达式跟其他表达式一样,必须在使用它之前赋值。不然会报错。
//函数的调用,会报错,提示函数未定义
sayHi();
var sayHi = function(){
console.log("hello");
}
function sayHi(){
console.log("hello" + arguments[0] + "," + arguments[1] );
}
所以在js中函数的命名参数只是提供了便利,但是并不是必须的。
function doAdd(num1, num2) {
if(arguments.length ==1) {
console.log(num1+10);
}else if(arguments.length == 2){
console.log(arguments[0]+num2);
}
}
//调用函数
doAdd(1, 2);
function doAdd(num1, num2) {
arguments[1] = 10;
console.log(arguments[1]+num2);
}
//调用函数
doAdd(1, 2);
//返回的是20,
我们对上面这个分析一下:
对于严格模式中,对arguments对象做了一些限制
函数中所有参数都是按值传递:这句话意思是说把函数外部的值复制给函数内部参数。就是把值从一个变量复制到另一个变量一样。
基本类型值的传递如同基本类型变量复制一样,而引用类型值的传递,则如果引用类型变量的复制一样。访问变量有按值和按引用两种方式,而参数只能按值传递。
看一个基本类型例子:
//基本类型
var addTen(num){
num+=10;
return num;
}
var count =20;
var result = addTen(count);
console.log(result);
//返回的是30
console.log(count);
//返回的是20,没有改变
再看一个引用类型例子
//引用类型
function setName(obj){
obj.name = "jerry";
obj = new Object();
obj.name = "gery";
}
var person = new Object();
setName(person);
console.log(person.name);
//返回的是jerry
基本类型很好理解,但是对用引用类型大家会疑惑,总是弄不清楚。我们可以对其分析一下
function addSomeNumber(num) {
return num+100;
}
function addSomeNumber(num) {
return num+200;
}
var result = addSomeNumber(100)
//结果是300
第二个函数覆盖第二个函数。
在定义函数的时候,不必指定是否有返回值,实际上,任何函数在任何时候都可以通过return语句后跟要返回的值来实现返回值。
function sum(num1, num2) {
return num1+num2;
}
在严格模式对函数有一些限制
递归函数是一个函数通过名字调用自身的情况下构成。
function factorial(num) {
if(num <=1) {
return 1;
}else {
return num*factorial(num-1);
}
}
//这上面一个是一个经典的递归函数,看上去并没有问题,但是如果这样的话
var factorial1 = factorial;
factorial =null;
console.log(factorial1(4));
//会报错
我们对上面这个进行分析一下,为什么会出现这样,是因为函数保存在变量factorial1中,再对这个函数赋值为null,因为这个是引用类型,所以这个是函数也是null,调用这个函数已经不存在,所以会报错。
对于上面这种情况,我们可以采用下面两种方式来解决
第一种:利用arguments.callee来实现,arguments.callee是代表一个正在执行的函数指针。因此可以用来实现对函数的递归调用---这个在严格模式下面会出错,因为严格模式下,不能通过脚本访问arguments.callee。
function factorial(num) {
if(num <=1) {
return 1;
}else {
return num*arguments.callee(num-1);
}
}
var factorial1 = factorial;
factorial =null;
console.log(factorial1(4));
//这样就不会报错了
第二种:利用函数的表达式
function factorial = (function f(num){
if(num <=1) {
return 1;
}else {
return num*f(num-1);
}
});
var factorial1 = factorial;
factorial = null;
console.log(factorial1(4));
//这样就不会报错了
在JavaScript中并没有块级作用域的概念,这就意味着在块语句中定义的变量,实际上是在包含函数中而非语句中创建的。
function outputNumbers(count){
for(var i=0; i<count; i++) {
console.log(i);
}
console.log(i);
}
我们分析一下这个:
即使错误的重新声明一个变量同一个变量,也不会改变它的值。
function outputNumbers(count){
for(var i=0; i<count; i++) {
console.log(i);
}
var i; //重新声明同一个变量
console.log(i);
}
JavaScript从来不会告诉你是否多次声明了同一个变量,遇到这种情况,它只会对后续的声明视而不见(不过,它会执行后续声明中的变量初始化)。对于这个问题,可以用匿名函数来模范块级作用域。
闭包:是有权访问另一个函数作用域中的变量的函数。
常见创建闭包的方式有:在一个函数内部创建另一个函数
(闭包的相关知识在作用域的那一节有了详细的介绍,不在介绍)
作用域的这种配置机制引出一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值,因为闭包所保存是整个变量对象,而不是某个特殊的变量。下面我们看个例子:
//创建了一个函数
function createFunction() {
//创建了一个数组
var result = new Array();
//一个for循环
for(var i=0; i<10; i++) {
//一个闭包
result[i] = function(){
//访问的是其包含函数的变量
return i;
}
}
}
返回的结果是一个数组,但是这个数组并不是[0,1..9] ,而是[10, 10..10],在每一个匿名函数的作用域链中都保存着createFunction()函数的活动对象,所以他们引用的是同一个变量i,所以当createFunction()函数返回后,变量i变成10,此时每一个匿名函数都引用着保存变量i的同一个变量对象。所以每个函数内部i的值都是10。
但是我们可以通过创建另一个匿名函数强制让闭包的行为符合预期
function createFunction() {
var result = new Array();
for(var i=0; i<10; i++) {
result[i] = function(num){
return function(){
return num;
}
}(i);
}
return result;
}
在这里,我们并没有直接把闭包赋值给数组,而是定义了一个匿名函数。并将立即执行该匿名函数的结果赋值给数组。
这里的匿名函数有一个参数num。也就是最终的函数要返回的值,在调用每个匿名函数时,我们传入了变量i,由于函数的参数是按值传递,所以就会将变量i的当前值复制给参数num,而在这个匿名函数内部,又创建并返回一个访问num的闭包,这样以来,result数组中的每一个函数都有自己num变量的一个副本,因此就可以返回各自不同的数值。
//块级作用域语法
(function(){
//这里是块级作用域
})();
将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式。而紧随其后的另一对圆括号会立即调用这个函数。可能这种语法有点不好理解。我们可以这样理解:
var count=5;
outputNumber(count);
//为了让代码更简洁
outputNumber(5);
//我们这样写,是因为变量只不过值的另一种表现形式。因此可以用实际值代替变量
var someFuncton = function(){
//这里是块级作用域
};
someFuncton();
//根据上面的思路,可以这样写
//实际值代替变量
function(){
//这里是块级作用域
}();
//但是,function是关键字是当作一个函数的声明的开始,函数声明后面不能跟圆括号。
//而函数的表达式后面可以跟圆括号,把函数声明转为函数表达式,只要像下面这样
(function(){
//这里是块级作用域
})();
无论在什么地方,只要临时需要一些变量,就可以使用私有作用域。例如:
function outputNumber(count) {
(function(){
for(var i=0; i<count; i++){
console.log(i);
}
})();
console.log(i)
//会出错的。
}
因为在for循环外部插入了一个私有作用域,在匿名函数中定义的任何变量,都会在执行结束时被销毁。因此,变量i只能在循环中使用,使用后即被销毁。
在大型应用程序中,我们都应该尽量少向全局作用域添加变量和函数,因为过多的全局变量和函数,很容易导致命名冲突。通过创建私有作用域,每个开发人员可以使用自己的变量,又不必担心搞乱全局。
且这种做法可以减少闭包占用的内存问题。因为没有匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。
严格来说,在javascript中没有私有成员的概念,所有对象的属性都是共有的,但是有一个私有变量的概念:任何函数中定义的变量,都可以认为是私有变量,因此不能在函数的外部访问这些变量。
私有变量:包括函数的参数,局部变量和函数内部定义的其他函数。
function add(num1, num2) {
var sum = num1 + num2;
return sum;
}
在函数的内部,我们定义了三个变量 sum, num1, num2。 在函数内部可以访问这几个变量,但是在函数的外部则不能访问他们。如果在函数内部建一个闭包,闭包通过自己的作用域链也可以访问到这些变量。我们可以利用这种,创建用于访问私有变量的共有方法。
我们把有权访问私有变量和私有函数的公有方法称为特权方法。有两种在对象上创建特权方法的方法。
第一种:在构造函数中定义特权方法
function MyObject(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//特权方法
this.publicMethod = function(){
privateVariable++;
return privateFunction();
}
}
对这个例子而言, privateVariable和privateFunction()只能通过特权方法publicMethod来访问。在创建
MyObject的实例后,除了使用publicMethod()方法这一个途径外,没有任何方法可以直接访问privateVariable和privateFunction()
利用私有和特有成员,我们可以隐藏那些不应该被直接修改的数据。例如:
function Person(name) {
this.getName = function(){
return name;
};
this.setName = function(value){
name = value;
};
}
var person = new Person("jerry");
console.log(person.getName());
//返回jerry
person.setName("gery");
console.log(person.getName());
//返回gery
以上代码定义了两个特权方法,getName()和setName(),这两个函数在构造函数外部也可以访问,而且有权访问私有变量name.但在构造函数的外部,没有任何办法访问name。
这两个方法是在构造函数内部定义的,他们作为闭包能够通过作用域链访问name,私有变量name在Person的每一个实例都是不同的,每次调用这个构造函数,都会重新创建这两个方法。
利用构造函数定义特权方法,有一个缺点,那就是必须使用构造函数,而构造函数模式针对每一组实例,都会创建同样一组新方法。
通过在私有作用域中定义私有变量和函数,同样可以创建特权方法。
(function(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//构造函数(注意这个是全局变量)
MyObject = function(){
};
//特权方法
MyObject.prototype.publicMethod = function(){
privateVariable++;
return privateFunction();
}
})();
这个模式创建一个私有作用域,并在其中封装了一个构造函数及相应的方法。我们把公有方法定义在原型上面,这里体现了原型模式,我们创建了函数的表达式,并没有创建函数声明,因为函数声明只能创建局部变量,,而且出于同样的原因,我们也没有在声明MyObject时,使用var关键字。
记住:初始化未经声明的变量,总会创建一个全局变量,所以MyObject是一个全局变量,但是,我们在严格模式下,给未经声明的变量赋值会报错的。
这个方法与刚才在构造函数中定义特权方法的区别在于:私有变量个函数是共享的。由于特权方法在原型上定义的,因此所有实例都使用同一个函数,而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用。
模块模式:为单例创建私有变量和特权方法。
单例:指的是只有一个实例的对象。按照惯例,javascript中以对象字面量的方式来创建单例对象。
var singleton = {
name : value,
method : function(){
//这里是方法
}
};
//模块模式通过为单例添加私有变量和特权方法,能够使其得到增强。
var singleton =function(){
//添加私有变量和函数
var privateVariable = 10;
function privateFunction() {
return false;
}
//特权方法
return {
publicProperty : true,
publicMethod : function(){
privateVariable;
privateFunction();
}
};
}();
这个模块模式使用了一个返回对象的匿名函数,在这个匿名函数,首先定义了私有变量和函数,然后将一个对象字面量作为函数返回。返回的对象字面量只包含可以公开的属性和方法,由于这个对象是在匿名函数内部定义的,因此它的公有方法有权访问私有变量和函数。
从本质上来讲:这个对象字面量定义的是单例的公共接口。这种模式在需要对单例初始化,同时又维护其私有变量时,是非常有用的。
如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,就可以使用模块模式。以这种模式创建的每一个单例都是Object的实例。因为最终要通过一个对象字面的方法来表示它。
这种增强的模块模式适合那些单例必须是某种类型的实例。同时还必须添加某些属性或方法对其加以增强的情况。
var singleton =function(){
//添加私有变量和函数
var privateVariable = 10;
function privateFunction() {
return false;
}
//创建对象
var object = new CustomType();
//特权方法
objectProperty : true,
objectMethod : function(){
privateVariable;
privateFunction();
//返回这个对象
return object;
}();
当前窗口的可视区域的高度+文档被卷曲的高度scrollTop = 文档内容的高度,相等的时候,此时就滚到到底部。
文档被卷曲的高度scrollTop为0的话,那么此时就滚到了顶部
<p id="content">Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Dolore ipsa, illo nesciunt porro sed repellat quibusdam aliquam nobis harum
officiis aperiam eum possimus adipisci officia expedita beatae omnis enim,
obcaecati!
</p>
p{
margin: 1000px auto 0;
width: 400px;
height: 400px;
background-color: #ccc;
}
window.onload = function() {
//可视区域高度
var viewH = document.documentElement.clientHeight || document.body.clientHeight;
// 文档的内容高度
var contentH = document.body.scrollHeight;
window.onscroll = function() {
// 当窗口开始滚动时。
var sTop = document.body.scrollTop ||document.documentElement.scrollTop;
// 文档卷曲的高度
if ((viewH+sTop) >= contentH ){
alert('滚到底部了');
}
if(sTop ==0 ) {
alert('滚到顶部了');
}
}
}
能力检测的目标不是识别特定的浏览器,而是识别浏览器的能力。采用这种方式不必顾及特定的浏览器如何如何,只要确定浏览器支持特定的能力,就可以给出解决方案。其基本模式如下:
举个例子,IE9以下的浏览器是不支持canvas的,此时我们通过能力检测,对不支持的浏览器做出相对应的解决方案。
var canvas = document.getElementById('canvas');
// 不支持的浏览器直接不显示
if (!canvas || !canvas.getContext) {
alert("您的浏览器太旧了,升级您的浏览器");
return false;
}
//不建议这样检测
function isSortable(object) {
return !!object.sort;
}
//建议通过这样检测
function isSortable(object) {
return typeof object.sort == 'function';
}
上面的例子中的第一种方法是通过检测对象是否存在sort()方法,来确定对象是否支持排序。问题是,任何包含sort属性的对象也会返回true。
检测某个属性是否存在并不能确定对象是否支持排序,更好的方式是检测sort是不是一个函数。也就是第二种方法。
检测某个或某几个特性并不能够确定浏览器。例如看下面的例子:
//错误,不够具体
var isFirefox = !!(navigator.vendor && navigator.vendorSub);
//错误,假设过头了
var isIE = !!(document.all && document.uniqueID);
实际上,根据浏览器不同的能力组合起来更可取,如果你知道自己的应用程序需要使用某些特定的浏览器特性,那么最好是一次性检测所有相关特性,而不要分别检测。
//确定浏览器是否支持Netscape
var hasNSPlugins = !!(navigator.plugins && navigator.plugins.length);
怪癖检测的目标是识别浏览器的特殊行为,怪癖检测是想要知道浏览器存在什么缺陷,这通常需要运行一小段代码,以确定某一特性不能正常工作。
例如在早期的IE8中,如果某个实例的属性与[[Enumerable]]标记为false的某个原型属性名同名,那么该实例属性将不会出现在for-in循环中,可以使用如下代码来检测这个怪癖
var hasDontEnumQuirk = function(){
var o = {
toString: function(){};
};
for(var prop in o) {
if(prop == 'toString') {
return fasle;
}
}
return ture
}();
用户代理检测也是争议最大的一种客户端检测技术。用户代理检测通过检测用户代理字符串来确定实际使用的浏览器。
在每一次HTTP请求的过程中,用户代理字符串是作为响应的首部发送,而且该字符串可以通过javascript的navigator.useAgent属性访问。
在服务器端,通过检测用户代理字符串来确定使用的浏览器是一种常用而且广为接受的做法,而在客户端,用户代理检测一般被的当做一种万不得以才用的做法,其优先级是在能力检测和(或)怪癖检测之后。
电子欺骗:浏览器通过在自己的用户代理字符串加入一些错误或者误导性信息,来达到欺骗服务器的目的。
Gecko是firefox的呈现引擎,当初的Gecko是作为通用Mozilla浏览器一部分开发的,而第一个采用Gecko引擎的浏览器是Netscape6;规定的用户代理字符串构成如下:
Mozilla/Mozilla 版本号 (平台; 加密类型; 操作系统或CPU; 语言; 预先发布版本)
Geoko/Geoko版本号 应用程序或产品/应用程序或产品版本号
在Firefox4以来,Mozilla简化了这个用户代理字符串
Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0
safari的呈现引擎加WebKit,是Linux平台中的Konqueror浏览器的呈现引擎KHTML的一个分支,几年后,WebKit独立出来成为了一个开源项目,专注于呈现引擎的开发。
Mozilla/5.0 (平台;加密类型;操作系统或cpu) AppleWebkit/AppleWebkit版本号(KHTML,like Gecko) safari/safari版本号
在windows下safari下打印如下:
Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.57.2 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2
与KDE Linux 集成的Konqueror,是一款基于KHTML开源呈现引擎的浏览器,尽管Konqueror只能在Linux中使用,但是也有数量可观的用户。
其代理字符串的格式如下:
Mozilla/5.0 (compatible; Konqueror/ 版本号; 操作系统或CPU )
下面是一个例子:
Mozilla/5.0 (compatible; ) Konqueror/3.5 SunOS) KHTML/3.5.0 (like Gecko)
谷歌公司的chrome浏览器以WebKit作为呈现引擎,但使用了不同的javascript引擎。
用户代理字符串完全自取webkit,只添加了一段表示chrome版本号信息;格式如下:
Mozilla/5.0 (平台;加密类型;操作系统或cpu) AppleWebKit/AppleWebKit版本号(KHTML,like Gecko) chrome/chrome版本号 safari/safari版本号
在windows下chrome下打印如下:
Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.152 Safari/537.36
Opera默认的用户代理字符串是所有现代浏览器中最合理的---正确地标识了自身极其版本号,在Opera8.0之前;其用户代理字符串采用如下格式:
Opera/ 版本号 (操作系统或CPU;加密类型) [语言]
Opera8发布后,用户代理字符串的 “语言”部分被移到圆括号内,以便更好地与其他浏览器匹配;如下所示:
Opera/版本号 (操作系统或CPU;加密类型;语言)
自从opera9以后,出现两种修改用户代理字符串的方式,一种是将自身标识为另一个浏览器,如Firefox或者IE,在这种方式下,用户代理字符就如同Firefox或者IE的字符串一样,只不过在末尾追加字符串opera以及opera版本号。
另一中方式就是把自己打扮成Firefox或者IE,即没有opera滋养那个,也不包含opera版本信息。在启用了身份隐瞒功能的情况下,无法将opera和其他浏览器区分开来。
opera10对代理字符串进行了修改,格式
Opera/9.80 (操作系统或CPU;加密类型;语言) presto/presto版本号 version/版本号
在现在的最新的版本opera29,
Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.152 Safari/537.36 OPR/29.0.1795.60
自从IE3以来,微软已经将IE的用户代理字符串修改成兼容Netscape的形式,结构如下:
Mozilla/版本号(平台;加密类型;操作系统或CPU)
IE11的如下:
mozilla/5.0 (windows nt 6.1; wow64; trident/7.0; slcc2; .net clr 2.0.50727; .net clr 3.5.30729; .net clr 3.0.30729; media center pc 6.0; infopath.3; .net4.0c; .net4.0e; rv:11.0) like gecko
移动操作系统IOS和Android默认的浏览器都是基于webkit;而且都像桌面版一样;共享相同的基本用户代理字符串格式;ios设备的基本格式如下:
Mozilla/5.0 (平台;加密类型;操作系统或CPU like Mac OS x;语言) AppleWebKit/ AppleWebKit版本号(KHTML,like Gecko) Version/浏览器版本号Mobile/移动版本号 Safari/Safari版本号
Android浏览器中默认格式与IOS格式相似,没有移动版本号(但有Mobile记号):
Mozilla/5.0 (Linux; U; Android 2.2; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1
确切知道浏览器的名字和版本号不如确切知道它使用的是什么呈现引擎。如Firefox和Camino都是使用相同的版本的Gecko,那他们一定支持相同的特性。
我们主要检测五大呈现引擎:IE,Gecko,WebKit,KHTML和Opera
var client = function(){
var engine = {
// 呈现引擎
ie: 0,
gecko:0,
webkit: 0,
khtml:0,
opera:0,
//其他版本号
ver: null
};
// 在此检测呈现引擎,平台和设备
return {
engine: engine
};
}();
在如上client对象字面量中,每个呈现引擎都对应着一个属性,属性值默认为0;如果检测到了那个呈现引擎,那么将以浮点数值形式将引擎的版本号写入相应的属性。而呈现引擎的完整版本被写入变量ver属性中;
要正确识别呈现引擎,关键是检测顺序要正确,用户代理字符串存在诸多不一致的地方,如果检测顺序不对,很可能会导致检测结果不正确,因此,第一步就是识别Opera,因为它的用户代理字符串有可能模仿其他浏览器;目前最新的opera浏览器版本是29,所以如下:
var ua = navigator.userAgent.toLowerCase();
if(ua.match(/opr\/([\d\.]+)/)) {
var result = ua.match(/opr\/([\d\.]+)/);
console.log(result)
console.log(result[1])
}
console.log(result);打印如下:
["opr/29.0.1795.60", "29.0.1795.60", index: 110, input: "mozilla/5.0 (windows nt 6.1; wow64) applewebkit/53…rome/42.0.2311.152 safari/537.36 opr/29.0.1795.60"];
console.log(result[1])打印如下:
29.0.1795.60
由此;我们可以这样编写代码;如下:
var ua = navigator.userAgent.toLowerCase();
if(ua.match(/opr\/([\d\.]+)/)) {
var result = ua.match(/opr\/([\d\.]+)/);
engine.ver = result[1];
engine.opera = parseFloat(engine.ver);
}
但是呢opera29之前的版本, Opera5+也有window.opera对象,所以我们也必须检测window.opera对象,我们可以调用version()方法可以返回一个表示浏览器版本的字符串;如下代码:
if(window.opera) {
engine.ver = window.opera.version();
engine.opera = parseFloat(engine.ver);
}
现在我们为了兼容之前及之后的opera浏览器,我们可以综合一下;如下:
var engine = client;
var ua = navigator.userAgent.toLowerCase();
if(ua.match(/opr\/([\d\.]+)/) || window.opera) {
var result = ua.match(/opr\/([\d\.]+)/);
engine.ver = result[1];
engine.opera = parseFloat(engine.ver);
if(window.opera) {
engine.ver = window.opera.version();
engine.opera = parseFloat(engine.ver);
}
}
现在第二步需要检测的是引擎是WebKit,因为WebKit的用户代理字符串中包含”Gecko”和”KHTML”这两个字符串,如果检测这两个,可能会有误差,但是WebKit的用户代理字符串中的”AppleWebKit”是独一无二的,因此可以根据这个来检测;
var engine = client;
var ua = navigator.userAgent.toLowerCase();
if(/applewebkit\/(\S+)/.test(ua)) {
engine.ver = RegExp["$1"];
engine.webkit = parseFloat(engine.ver);
}
因此综合以上的所有代码如下:
var engine = client;
var ua = navigator.userAgent.toLowerCase();
if(ua.match(/opr\/([\d\.]+)/) || window.opera) {
var result = ua.match(/opr\/([\d\.]+)/);
engine.ver = result[1];
engine.opera = parseFloat(engine.ver);
if(window.opera) {
engine.ver = window.opera.version();
engine.opera = parseFloat(engine.ver);
}
}else if(/applewebkit\/(\S+)/.test(ua)) {
engine.ver = RegExp["$1"];
engine.webkit = parseFloat(engine.ver);
}
下来要测试的呈现引擎是KHTML,同样,KHTML的用户代理字符串中也包含”Gecko”,因此在排除KHTML之前,我们无法准确检测基于Gecko浏览器。KHTML的版本号与WebKit的版本号在用户代理字符串中的格式差不多,因此也可以使用正则表达式,此外,由于Konqueror 3.1 及更早版本中不包含KHTML 的版本,故而就要使用Konqueror 的版本来代替。下面就是相应的检测代码。
if (/KHTML\/(\S+)/.test(ua) || /Konqueror\/([^;]+)/.test(ua)){
engine.ver = RegExp["$1"];
engine.khtml = parseFloat(engine.ver);
}
下面是所有的代码
var engine = client;
var ua = navigator.userAgent.toLowerCase();
if(ua.match(/opr\/([\d\.]+)/) || window.opera) {
var result = ua.match(/opr\/([\d\.]+)/);
engine.ver = result[1];
engine.opera = parseFloat(engine.ver);
if(window.opera) {
engine.ver = window.opera.version();
engine.opera = parseFloat(engine.ver);
}
}else if(/applewebkit\/(\S+)/.test(ua)) {
engine.ver = RegExp["$1"];
engine.webkit = parseFloat(engine.ver);
}else if (/khtml\/(\S+)/.test(ua) || /konqueror\/([^;]+)/.test(ua)){
engine.ver = RegExp["$1"];
engine.khtml = parseFloat(engine.ver);
}
在排除WebKit和KHTML后,就可以准确检测Gecko了,但是在用户代理字符串中,Gecko的版本号不会出现在字符串”Gecko”的后面,而是会出现在字符串”rv:”的后面。因此必须使用一个更复杂的正则表达式;
比如 firefox下的用户代理如下:
Mozilla/5.0 (Windows NT 6.1; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0
如下JS代码检测:
var engine = client;
var ua = navigator.userAgent.toLowerCase();
if(/rv:([^\)]+)\) gecko\/\d{8}/.test(ua)){
engine.ver = RegExp["$1"];
engine.gecko = parseFloat(engine.ver);
}
所以所有的JS代码如下:
var engine = client;
var ua = navigator.userAgent.toLowerCase();
if(ua.match(/opr\/([\d\.]+)/) || window.opera) {
var result = ua.match(/opr\/([\d\.]+)/);
engine.ver = result[1];
engine.opera = parseFloat(engine.ver);
if(window.opera) {
engine.ver = window.opera.version();
engine.opera = parseFloat(engine.ver);
}
}else if(/applewebkit\/(\S+)/.test(ua)) {
engine.ver = RegExp["$1"];
engine.webkit = parseFloat(engine.ver);
}else if (/khtml\/(\S+)/.test(ua) || /konqueror\/([^;]+)/.test(ua)){
engine.ver = RegExp["$1"];
engine.khtml = parseFloat(engine.ver);
}else if(/rv:([^\)]+)\) gecko\/\d{8}/.test(ua)){
engine.ver = RegExp["$1"];
engine.gecko = parseFloat(engine.ver);
}
最后一个检测就是IE浏览器了,IE的版本号位于字符串”msie”的后面,一个分号的前面;如下:
mozilla/5.0 (compatible; msie 9.0; windows nt 6.1; wow64; trident/7.0; slcc2; .net clr 2.0.50727; .net clr 3.5.30729; .net clr 3.0.30729; media center pc 6.0; infopath.3; .net4.0c; .net4.0e)
如下JS代码检测:
if (/msie ([^;]+)/.test(ua)){
engine.ver = RegExp["$1"];
engine.ie = parseFloat(engine.ver);
}
如上;所有的代码如下:
var client = function(){
var engine = {
// 呈现引擎
ie: 0,
gecko:0,
webkit: 0,
khtml:0,
opera:0,
//其他版本号
ver: null
};
// 在此检测呈现引擎,平台和设备
return {
engine: engine
};
}();
var engine = client;
var ua = navigator.userAgent.toLowerCase();
if(ua.match(/opr\/([\d\.]+)/) || window.opera) {
var result = ua.match(/opr\/([\d\.]+)/);
engine.ver = result[1];
engine.opera = parseFloat(engine.ver);
if(window.opera) {
engine.ver = window.opera.version();
engine.opera = parseFloat(engine.ver);
}
}else if(/applewebkit\/(\S+)/.test(ua)) {
engine.ver = RegExp["$1"];
engine.webkit = parseFloat(engine.ver);
}else if (/khtml\/(\S+)/.test(ua) || /konqueror\/([^;]+)/.test(ua)){
engine.ver = RegExp["$1"];
engine.khtml = parseFloat(engine.ver);
}else if(/rv:([^\)]+)\) gecko\/\d{8}/.test(ua)){
engine.ver = RegExp["$1"];
engine.gecko = parseFloat(engine.ver);
}else if (/msie ([^;]+)/.test(ua)){
engine.ver = RegExp["$1"];
engine.ie = parseFloat(engine.ver);
}
大多数情况下,如上面识别了引擎还不能满足我们的需求,比如苹果公司的safari浏览器和谷歌的chrome浏览器都使用了webkit作为呈现引擎;但他们的javascript引擎且不一样,在这两个浏览器中,client.webkit都会返回非0值,无法区别,因此我们还需要识别下浏览器;
var client = function(){
var engine = {
// 呈现引擎
ie: 0,
gecko:0,
webkit: 0,
khtml:0,
opera:0,
//其他版本号
ver: null
};
var browser = {
// 浏览器
ie: 0,
firefox:0,
safari:0,
konq:0,
opera:0,
chrome:0,
// 其他的版本
ver: null
};
// 在此检测呈现引擎,平台和设备
return {
engine: engine,
browser: browser
};
}();
上代码增加了私有变量browser,用于保存每个主要浏览器的属性,与engine变量一样,除了当前使用的浏览器,其他属性值将保持为0;如果是当前使用的浏览器,则这个属性中保存的是浮点数值形式的版本号,同样browser中的ver属性中在必要时将包含字符串形式的浏览器完整版本号;
因此封装后的所有JS代码如下:
var client = function(){
var engine = {
// 呈现引擎
ie: 0,
gecko:0,
webkit: 0,
khtml:0,
opera:0,
//其他版本号
ver: null
};
var browser = {
// 浏览器
ie: 0,
firefox:0,
safari:0,
konq:0,
opera:0,
chrome:0,
// 其他的版本
ver: null
};
// 在此检测呈现引擎,平台和设备
return {
engine: engine,
browser: browser
};
}();
var engine = client;
var browser = client;
var ua = navigator.userAgent.toLowerCase();
if(ua.match(/opr\/([\d\.]+)/) || window.opera) {
var result = ua.match(/opr\/([\d\.]+)/);
engine.ver = browser.ver = result[1];
engine.opera = browser.opera = parseFloat(engine.ver);
if(window.opera) {
engine.ver = browser.ver = window.opera.version();
engine.opera = browser.opera = parseFloat(engine.ver);
}
}else if(/applewebkit\/(\S+)/.test(ua)) {
engine.ver = RegExp["$1"];
engine.webkit = parseFloat(engine.ver);
// 确定是chrome还是safari
/*
* chrome用户代理字符串
* Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko)
* Chrome/42.0.2311.152 Safari/537.36
*/
if(/chrome\/(\S+)/.test(ua)) {
browser.ver = RegExp["$1"];
browser.chrome = parseFloat(browser.ver);
}else if(/version\/(\S+)/.test(ua)) {
/*
* safari用户代理字符串
* Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.57.2 (KHTML, like Gecko)
* Version/5.1.7 Safari/534.57.2
*/
browser.ver = RegExp["$1"];
browser.safari = parseFloat(browser.ver);
}else {
//近似地确定版本号
var safariVersion = 1;
if (engine.webkit < 100){
safariVersion = 1;
} else if (engine.webkit < 312){
safariVersion = 1.2;
} else if (engine.webkit < 412){
safariVersion = 1.3;
} else {
safariVersion = 2;
}
browser.safari = browser.ver = safariVersion;
}
}else if (/khtml\/(\S+)/.test(ua) || /konqueror\/([^;]+)/.test(ua)){
engine.ver = browser.ver = RegExp["$1"];
engine.khtml = browser.konq = parseFloat(engine.ver);
}else if(/rv:([^\)]+)\) gecko\/\d{8}/.test(ua)){
engine.ver = RegExp["$1"];
engine.gecko = parseFloat(engine.ver);
/*
* firefox的用户代理的字符串
* Mozilla/5.0 (Windows NT 6.1; WOW64; rv:38.0)
* Gecko/20100101 Firefox/38.0
*/
// 确定是不是firefox
if(/firefox\/(\S+)/.test(ua)) {
browser.ver = RegExp["$1"];
browser.firefox = parseFloat(browser.ver);
}
}else if (/msie ([^;]+)/.test(ua) || "ActiveXObject" in window){
if("ActiveXObject" in window) {
if(/msie ([^;]+)/.test(ua)) {
engine.ver = browser.ver = RegExp["1"]; engine.ie = browser.ie = parseFloat(engine.ver); }else { // ie11+ if(/rv:([^\)]+)\)/.test(ua)) { engine.ver = browser.ver = RegExp["1"]; engine.ie = browser.ie = parseFloat(engine.ver); }else { // ie11+ if(/rv:([^\)]+)\)/.test(ua)) { engine.ver = browser.ver = RegExp["1"];
engine.ie = browser.ie = parseFloat(engine.ver);
}
}
}
}
// 可以打印下
console.log(browser);
对opera和IE而言,browser对象中的值等于engine对象中值,对Konqueror 而言,browser.konq 和browser.ver 属性分别等于engine.khtml 和engine.ver 属性。为了检测Chrome 和Safari,我们在检测引擎的代码中添加了if 语句。提取Chrome 的版本号时,需要查找字符串"chrome/"并取得该字符串后面的数值。而提取Safari 的版本号时,则需要查找字符串"version/"并取得其后的数值。由于这种方式仅适用于Safari 3 及更高版本,因此需要一些备用的代码,将WebKit 的版本号近似地映射为Safari 的版本号(至于else后面的就不多讲)。在检测Firefox 的版本时,首先要找到字符串"firefox/",然后提取出该字符串后面的数值。
有了上面的代码,我们就可以编写下面的代码判断了,如下代码:
if (client.engine.webkit) {
//if it’s WebKit
if (client.browser.chrome){
//执行针对Chrome 的代码
} else if (client.browser.safari){
//执行针对Safari 的代码
}
} else if (client.engine.gecko){
if (client.browser.firefox){
//执行针对Firefox 的代码
} else {
//执行针对其他Gecko 浏览器的代码
}
}
目前有三大主流平台(windows,Mac,Unix(包括各种linux));因为那些浏览器(safari,opera,firefox)在不同的平台可能会有不同的问题;为了检测这些平台,还需要像下面这样再添加一个新对象;
var client = function(){
var engine = {
// 呈现引擎
ie: 0,
gecko:0,
webkit: 0,
khtml:0,
opera:0,
//其他版本号
ver: null
};
var browser = {
// 浏览器
ie: 0,
firefox:0,
safari:0,
konq:0,
opera:0,
chrome:0,
// 其他的版本
ver: null
};
var system = {
win: false,
mac: false,
xll: false
};
// 在此检测呈现引擎,平台和设备
return {
engine: engine,
browser: browser,
system: system
};
}();
如上的代码添加了一个包含3个属性的新变量system,其中,win属性表示是否为windows平台,mac代表Mac,xll代表是Unix,system的对象的属性默认都为false,在确定平台时,检测navigator.platform要比检测用户代理字符串更简单,检测用户代理字符串在不同的浏览器中会给出不同的平台信息,而navigator.platform属性可能的值包括”Win32”,”Win64”,”MacPPC”,”MacIntel”,”Xll”和”Linux i686”,这些值在不同的浏览器中都是一致的,检测代码非常直观;如下代码:
var system = client;
var platform = navigator.platform;
system.win = platform.indexOf("Win") == 0;
system.mac = platform.indexOf("Mac") == 0;
system.x11 = (platform.indexOf("X11") == 0) || (platform.indexOf("Linux") == 0);
可以通过简单地检测字符串”iPhone”,”iPod”,”iPad”,就可以分别设置响应属性的值了
system.iphone = ua.indexOf(“iphone”) > -1;
system.ipod = ua.indexOf(“ipod”) > -1;
system.ipad = ua.indexOf(“ipad”) > -1;
除了知道iOS 设备,最好还能知道iOS 的版本号。在iOS 3 之前,用户代理字符串中只包含"CPU like
Mac OS",后来iPhone 中又改成"CPU iPhone OS 3_0 like Mac OS X",iPad 中又改成"CPU OS 3_2
like Mac OS X"。也就是说,检测iOS 需要正则表达式反映这些变化。
//检测iOS 版本
if (system.mac && ua.indexOf("mobile") > -1){
if (/cpu (?:iphone )?os (\d+_\d+)/.test(ua)){
system.ios = parseFloat(RegExp.$1.replace("_", "."));
} else {
system.ios = 2; //不能真正检测出来,所以只能猜测
}
}
检查系统是不是Mac OS、字符串中是否存在"Mobile",可以保证无论是什么版本,system.ios中都不会是0。然后,再使用正则表达式确定是否存在iOS 的版本号。如果有,将system.ios 设置为表示版本号的浮点值;否则,将版本设置为2。(因为没有办法确定到底是什么版本,所以设置为更早的版本比较稳妥。)检测Android 操作系统也很简单,也就是搜索字符串"Android"并取得紧随其后的版本号。
//检测Android 版本
if (/android (\d+\.\d+)/.test(ua)){
system.android = parseFloat(RegExp.$1);
}
由于所有版本的Android 都有版本值,因此这个正则表达式可以精确地检测所有版本,并system.android 设置为正确的值。在了解这些设备信息的基础上,就可以通过下列代码来确定用户使用的是什么设备中的WebKit 来。
if (client.engine.webkit){
if (client.system.ios){
//iOS 手机的内容
} else if (client.system.android){
//Android 手机的内容
}
}
因此所有的JS代码如下:
var client = function(){
var engine = {
// 呈现引擎
ie: 0,
gecko:0,
webkit: 0,
khtml:0,
opera:0,
//其他版本号
ver: null
};
var browser = {
// 浏览器
ie: 0,
firefox:0,
safari:0,
konq:0,
opera:0,
chrome:0,
// 其他的版本
ver: null
};
var system = {
win: false,
mac: false,
xll: false,
// 移动设备
iphone: false,
ipod: false,
ipad: false,
ios: false,
android:false
};
// 在此检测呈现引擎,平台和设备
return {
engine: engine,
browser: browser,
system: system
};
}();
var engine = client;
var browser = client;
var ua = navigator.userAgent.toLowerCase();
if(ua.match(/opr\/([\d\.]+)/) || window.opera) {
var result = ua.match(/opr\/([\d\.]+)/);
engine.ver = browser.ver = result[1];
engine.opera = browser.opera = parseFloat(engine.ver);
if(window.opera) {
engine.ver = browser.ver = window.opera.version();
engine.opera = browser.opera = parseFloat(engine.ver);
}
}else if(/applewebkit\/(\S+)/.test(ua)) {
engine.ver = RegExp["$1"];
engine.webkit = parseFloat(engine.ver);
// 确定是chrome还是safari
/*
* chrome用户代理字符串
* Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko)
* Chrome/42.0.2311.152 Safari/537.36
*/
if(/chrome\/(\S+)/.test(ua)) {
browser.ver = RegExp["$1"];
browser.chrome = parseFloat(browser.ver);
}else if(/version\/(\S+)/.test(ua)) {
/*
* safari用户代理字符串
* Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.57.2 (KHTML, like Gecko)
* Version/5.1.7 Safari/534.57.2
*/
browser.ver = RegExp["$1"];
browser.safari = parseFloat(browser.ver);
}else {
//近似地确定版本号
var safariVersion = 1;
if (engine.webkit < 100){
safariVersion = 1;
} else if (engine.webkit < 312){
safariVersion = 1.2;
} else if (engine.webkit < 412){
safariVersion = 1.3;
} else {
safariVersion = 2;
}
browser.safari = browser.ver = safariVersion;
}
}else if (/khtml\/(\S+)/.test(ua) || /konqueror\/([^;]+)/.test(ua)){
engine.ver = browser.ver = RegExp["$1"];
engine.khtml = browser.konq = parseFloat(engine.ver);
}else if(/rv:([^\)]+)\) gecko\/\d{8}/.test(ua)){
engine.ver = RegExp["$1"];
engine.gecko = parseFloat(engine.ver);
/*
* firefox的用户代理的字符串
* Mozilla/5.0 (Windows NT 6.1; WOW64; rv:38.0)
* Gecko/20100101 Firefox/38.0
*/
// 确定是不是firefox
if(/firefox\/(\S+)/.test(ua)) {
browser.ver = RegExp["$1"];
browser.firefox = parseFloat(browser.ver);
}
}else if (/msie ([^;]+)/.test(ua) || "ActiveXObject" in window){
if("ActiveXObject" in window) {
if(/msie ([^;]+)/.test(ua)) {
engine.ver = browser.ver = RegExp["$1"];
engine.ie = browser.ie = parseFloat(engine.ver);
}else {
if(/rv:([^\)]+)\)/.test(ua)) {
engine.ver = browser.ver = RegExp["$1"];
engine.ie = browser.ie = parseFloat(engine.ver);
}
}
}
}
// 检测平台
var system = client;
var platform = navigator.platform;
system.win = platform.indexOf("Win") == 0;
system.mac = platform.indexOf("Mac") == 0;
system.x11 = (platform.indexOf("X11") == 0) || (platform.indexOf("Linux") == 0);
// 移动设备
system.iphone = ua.indexOf("iphone") > -1;
system.ipod = ua.indexOf("ipod") > -1;
system.ipad = ua.indexOf("ipad") > -1;
//检测iOS 版本
if (system.mac && ua.indexOf("mobile") > -1){
if (/cpu (?:iphone )?os (\d+_\d+)/.test(ua)){
system.ios = parseFloat(RegExp.$1.replace("_", "."));
} else {
system.ios = 2; //不能真正检测出来,所以只能猜测
}
}
//检测Android 版本
if (/android (\d+\.\d+)/.test(ua)){
system.android = parseFloat(RegExp.$1);
}
<<javascript高级程序设计>>
javascript客户端检测技术
每个函数都包含两个非继承来的方法:apply()和call()方法。
这两个方法的用途都是在特定的作用域运行函数,实际上等同于设置函数体内this对象的值。
这两个方法都接受两个参数,一个是在其中运行函数的作用域, 另一个是参数数组(第二个参数可以是Array实例,也可以是arguments对象)。
function sum(num1, num2) {
return num1+num2;
}
//使用apply,第二参数为arguments对象
function callSum1(num1, num2){
return sum.apply(this, arguments);
}
//使用apply,第二参数为数组
function callSum2(num1, num2){
return sum.apply(this, [num1, num2]);
}
console.log(callSum1(10, 10));
//返回20
console.log(callSum2(10, 10));
//返回20
在上面的那个例子,callSum方法调用sum函数时,传递的参数为this。因为在全局作用域调用,所以传入就是window对象。注意,在严格模式下,为指定环境对象,而调用函数,其this值不会自动转为window对象,除非明确把函数添加到某个对象或者条用apply()和call(),否则this是undefined。
call方法与apply()方法作用是相同的,他们的区别仅仅是接受参数的方式不同,对于call方法,第一个参数是一样的,变化的是其他参数都是直接传递给函数。那就是说在使用call方法的时候,参数要一个个的列出来。
function sum(num1, num2) {
return num1+num2;
}
function callSum1(num1, num2){
return sum.apply(this, num1, num2);
}
console.log(callSum1(10, 10));
//返回20
这两个方法,是取决于你传递参数的方法,那个方便用那个。
window.color = "red";
var o = {
color : "blue"
}
function sayColor() {
console.log(this.color)
}
sayColor(); //red
sayColor.call(o); //blue
sayColor.call(window); //red
sayColor.call(this) //red
使用这两个方法的来扩充函数作用域的最大的好处那就是,就是对象不需要与和方法有任何耦合关系。
在ECMAScript5中规定定义了bind()方法,该方法会创建一个函数的实例,其this值会被绑定到传给bind()函数的值。
window.oclor = "red";
var o = {
color : “blue”
};
function sayColor(){
console.log(this.color);
}
var color1 = sayColor.bind(o);
color1();
//返回的是blue
写这篇文章是因为,在学习百度ife的课程中,用原生的javascript来写一个焦点轮播图的时候,总是获取不到元素的定位left的属性,在控制台调试的时候left总是空字符,疑惑了半天,就在想为什么一直获取不到,我的css里面不是写了的吗?突然想到在那本书上看到,javascript的style对象只能获取,写在元素上面的样式例如
<div style="width"></div>
然后就上网搜索一下,看了张鑫鑫那篇文章有了大概的了解。然后又想起来,在《javascript高级程序设计(第三版)》中的12章对其有详细的介绍。那么我就想好好的把有关样式的知识了解清楚。
var supportsDOM2CSS = document.implementation.hasFeture("CSS","2.0");
var supportsDOM2CSS2 = document.implementation.hasFeture("CSS2","2.0");
一: 支持style特性的HTML的元素在javascript都有一个对应的style对象,但是这个得到的样式并包括通 过外部样式表与嵌入样式表经层叠而来的样式。
例如:
var div = document.getElementById("div");
div.style.color = "red";
//获得相应的css属性,需要把css的属性转为驼峰式命名法
div.style.backgroundColor = "#ccc";
注意大多数属性都可通过上面转换得到,但是有一个属性是例外的,那就是float,因为这个是js中的
保留字,因此不能作为属性名。在DOM2级样式表中,规定其属性名为cssFloat,而在IE中的其属性名为styleFloat;
如果没有为元素呢设置style特性,那么style对象可能包含一些默认值,但是这些在并不能准确反映该元素的样式信息。
二: DOM2级样式规范还为style对象
第一: cssText 访问到style特性的css代码以及设置
div.style.cssText = "width:200px, height:100px; background-color:#ccc";
设置cssText为应用多项变化最便捷的方式。
第二: length 给定元素的css属性的数量
第三: item(index) 给定位置的css属性的名称
第四: getPropertyValue(propertyName) 给定位属性的字符串值
第五: getPropertyCSSValue(propertyName) 给定位属性值的CSSvalue对象
for (var i = 0; i < div.style.length; i++) {
//方括号访问法
console.log(div.style[i]);
//item访问法
console.log(div.style.item(i));
}
var prop;
for (var i = 0; i < div.style.length; i++) {
//取得属性名
prop = div.style[i];
//取得属性值
value = div.style. getPropertyValue(prop);
}
CSSValue对象是包含两个属性的一个是cssText,一个是cssValueType,其中cssText的属性与getPropertyValue(propertyName) 是一样的。cssValueType是一个数值常量。有3中:0代表继承的值
1代表基本的值,2表示值的列表,3自定义的值。
第六: removeProperty(propertyName) 从样式中删除定位属性
第七: setProperty(propertyName, value, priority) 给定的属性设置相应的值,并加上有限特权(important或者一个空的字符串)
1. .defaultView.getComputedStyle是一个可以获取当前元素所有最终使用的CSS属性值。返回的是一个CSS样式声明对象([object CSSStyleDeclaration]),只读。在IE6-8上面是不支持的。
2. currentStyle是IE浏览器,element.currentStyle。获取最终CSS属性值(包括外链CSS文件,页面中嵌入的<style>属性等)。等同于getComputedStyle的值。
例如:
var myDiv = document.getElementById("myDiv");
var computedStyle = document.defaultView.getComputedStyle(myDiv, null);
alert(computedStyle.backgroundColor); //"red"
alert(computedStyle.width); //"100px"
alert(computedStyle.height); //"200px"
alert(computedStyle.border); //在某些浏览器中是“1px solid black”
感谢张鑫鑫的分享的文章,让我有了更进一步的认识
http://www.zhangxinxu.com/wordpress/2012/05/getcomputedstyle-js-getpropertyvalue-currentstyle/
这个 isPrototypeOf()方法是用来检查某个对象是否为某个对象的原型。如果是的话,那么返回true,如果不是的话,那么返回false。
Person.isPrototypeOf(person1);
//Person是否为person1的原型,如果是的话,返回true。不是的话,返回false;
取得原型对象。
var proto = {};
var obj = Object.create(proto);
Object.getPrototypeOf(obj) === proto; // true
1.hasPrototypeProperty这个方法是在,给定属性存在与对象原型中,才返回true,不然返回false。
2.hasOwnProperty这个方法是在,给定属性存在与对象实例中,才返回true,不然返回false。
for in循环时,返回的是所有能够通过对象访问的,可枚举的属性。其中既包括存在与实例中的属性,也包括了存在与原型中的属性。这里屏蔽了原型中不可枚举属性的实例属性也会在for in中出现。因为所有开发人员定义的属性都是可枚举的。(注意在这里IE8及更早的浏览器中有一个bug,那就是屏蔽不可枚举的实例属性不会出现在for in 中)
该方法接受一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
该方法会得到所有实例属性,无论它是否可枚举。
该方法接受两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。
var person = {
friends: ["a", "b"]
};
var test1 = Object.create(person);
test1.friends.push("c");
console.log(person.friends); //["a", "b", "c"]
第一种方式仅仅是在jQuery命名空间或者理解成jQuery身上添加了一个静态方法而以。所以我们调用通过$.extend()添加的函数时直接通过$符号调用($.myfunction())而不需要选中DOM元素。$('#example').myfunction()
$.extend({
sayHello: function(name) {
console.log('Hello,' + (name ? name : 'Dude') + '!');
}
})
$.sayHello(); //调用
$.sayHello('Wayou'); //带参调用
上面代码中,通过$.extend()向jQuery添加了一个sayHello函数,然后通过$直接调用。到此你可以认为我们已经完成了一个简单的jQuery插件了。但如你所见,这种方式用来定义一些辅助方法是比较方便的。比如一个自定义的console,输出特定格式的信息,定义一次后可以通过jQuery在程序中任何需要的地方调用它。
$.extend({
log: function(message) {
var now = new Date(),
y = now.getFullYear(),
m = now.getMonth() + 1, //!JavaScript中月分是从0开始的
d = now.getDate(),
h = now.getHours(),
min = now.getMinutes(),
s = now.getSeconds(),
time = y + '/' + m + '/' + d + ' ' + h + ':' + min + ':' + s;
console.log(time + ' My App: ' + message);
}
})
$.log('initializing...'); //调用
但这种方式无法利用jQuery强大的选择器带来的便利,要处理DOM元素以及将插件更好地运用于所选择的元素身上,还是需要使用第二种开发方式。你所见到或使用的插件也大多是通过此种方式开发。
基本方法:
$.fn.pluginName = function() {
//your code goes here
}
基本上就是往$.fn上面添加一个方法,名字是我们的插件名称。然后我们的插件代码在这个方法里面展开。比如我们将页面上所有链接颜色转成红色,则可以这样写这个插件:
$.fn.myPlugin = function() {
//在这里面,this指的是用jQuery选中的元素
//example :$('a'),则this=$('a')
this.css('color', 'red');
}
在插件名字定义的这个函数内部,this指代的是我们在调用该插件时,用jQuery选择器选中的元素,一般是一个jQuery类型的集合。比如$('a')返回的是页面上所有a标签的集合,且这个集合已经是jQuery包装类型了,也就是说,在对其进行操作的时候可以直接调用jQuery的其他方法而不需要再用美元符号来包装一下。
所以在上面插件代码中,我们在this身上调用jQuery的css()方法,也就相当于在调用 $('a').css()。
理解this在这个地方的含义很重要。这样你才知道为什么可以直接商用jQuery方法同时在其他地方this指代不同时我们又需要用jQuery重新包装才能调用,下面会讲到。初学容易被this的值整晕,但理解了就不难。
现在就可以去页面试试我们的代码了,在页面上放几个链接,调用插件后链接字体变成红色。
<ul>
<li>
<a href="http://www.webo.com/liuwayong">我的微博</a>
</li>
<li>
<a href="http://http://www.cnblogs.com/Wayou/">我的博客</a>
</li>
<li>
<a href="http://wayouliu.duapp.com/">我的小站</a>
</li>
</ul>
<p>这是p标签不是a标签,我不会受影响</p>
<script src="jquery-1.11.0.min.js"></script>
<script src="jquery.myplugin.js"></script>
<script type="text/javascript">
$(function(){
$('a').myPlugin();
})
</script>
下面进一步,在插件代码里处理每个具体的元素,而不是对一个集合进行处理,这样我们就可以针对每个元素进行相应操作。
我们已经知道this指代jQuery选择器返回的集合,那么通过调用jQuery的.each()方法就可以处理合集中的每个元素了,但此刻要注意的是,在each方法内部,this指带的是普通的DOM元素了,如果需要调用jQuery的方法那就需要用$来重新包装一下。
比如现在我们要在每个链接显示链接的真实地址,首先通过each遍历所有a标签,然后获取href属性的值再加到链接文本后面。
$.fn.myPlugin = function() {
//在这里面,this指的是用jQuery选中的元素
this.css('color', 'red');
this.each(function() {
//对每个元素进行操作
$(this).append(' ' + $(this).attr('href'));
}))
}
我们都知道jQuery一个时常优雅的特性是支持链式调用,选择好DOM元素后可以不断地调用其他方法。
要让插件不打破这种链式调用,只需return一下即可。
$.fn.myPlugin = function() {
//在这里面,this指的是用jQuery选中的元素
this.css('color', 'red');
return this.each(function() {
//对每个元素进行操作
$(this).append(' ' + $(this).attr('href'));
}))
}
一个强劲的插件是可以让使用者随意定制的,这要求我们提供在编写插件时就要考虑得全面些,尽量提供合适的参数。
比如现在我们不想让链接只变成红色,我们让插件的使用者自己定义显示什么颜色,要做到这一点很方便,只需要使用者在调用的时候传入一个参数即可。同时我们在插件的代码里面接收。另一方面,为了灵活,使用者可以不传递参数,插件里面会给出参数的默认值。
在处理插件参数的接收上,通常使用jQuery的extend方法,上面也提到过,但那是给extend方法传递单个对象的情况下,这个对象会合并到jQuery身上,所以我们就可以在jQuery身上调用新合并对象里包含的方法了,像上面的例子。当给extend方法传递一个以上的参数时,它会将所有参数对象合并到第一个里。同时,如果对象中有同名属性时,合并的时候后面的会覆盖前面的。
利用这一点,我们可以在插件里定义一个保存插件参数默认值的对象,同时将接收来的参数对象合并到默认对象上,最后就实现了用户指定了值的参数使用指定的值,未指定的参数使用插件默认值。
$.fn.myPlugin = function(options) {
var defaults = {
'color': 'red',
'fontSize': '12px'
};
var settings = $.extend(defaults, options);
return this.css({
'color': settings.color,
'fontSize': settings.fontSize
});
}
现在,我们调用的时候指定颜色,字体大小未指定,会运用插件里的默认值12px。
//覆盖默认的颜色
$('a').myPlugin({
'color': '#2C9929'
});
//覆盖默认的颜色和字体大小
$('a').myPlugin({
'color': '#2C9929',
'fontSize': '20px'
});
注意到上面代码调用extend时会将defaults的值改变,这样不好,因为它作为插件因有的一些东西应该维持原样,另外就是如果你在后续代码中还要使用这些默认值的话,当你再次访问它时它已经被用户传进来的参数更改了。
一个好的做法是将一个新的空对象做为$.extend的第一个参数,defaults和用户传递的参数对象紧随其后,这样做的好处是所有值被合并到这个空对象上,保护了插件里面的默认值。
$.fn.myPlugin = function(options) {
var defaults = {
'color': 'red',
'fontSize': '12px'
};
var settings = $.extend({},defaults, options);//将一个空对象做为第一个参数
return this.css({
'color': settings.color,
'fontSize': settings.fontSize
});
}
到此,插件可以接收和处理参数后,就可以编写出更健壮而灵活的插件了。若要编写一个复杂的插件,代码量会很大,如何组织代码就成了一个需要面临的问题,没有一个好的方式来组织这些代码,整体感觉会杂乱无章,同时也不好维护,所以将插件的所有方法属性包装到一个对象上,用面向对象的思维来进行开发,无疑会使工作轻松很多。
为什么要有面向对象的思维,因为如果不这样,你可能需要一个方法的时候就去定义一个function,当需要另外一个方法的时候,再去随便定义一个function,同样,需要一个变量的时候,毫无规则地定义一些散落在代码各处的变量。
还是老问题,不方便维护,也不够清晰。当然,这些问题在代码规模较小时是体现不出来的。
如果将需要的重要变量定义到对象的属性上,函数变成对象的方法,当我们需要的时候通过对象来获取,一来方便管理,二来不会影响外部命名空间,因为所有这些变量名还有方法名都是在对象内部。
接着上面的例子,我们可以把这个插件抽象成一个美化页面的对象,因为他的功能是设置颜色啊字体啊什么的,当然我们还可以加入其他功能比如设置下划线啊什么的。当然对于这个例子抽象成对象有点小题大做,这里仅作演示用。
所以我们新建一个对象命名为Beautifier,然后我们在插件里使用这个对象来编码。
//定义Beautifier的构造函数
var Beautifier = function(ele, opt) {
this.$element = ele,
this.defaults = {
'color': 'red',
'fontSize': '12px',
'textDecoration':'none'
},
this.options = $.extend({}, this.defaults, opt)
}
//定义Beautifier的方法
Beautifier.prototype = {
beautify: function() {
return this.$element.css({
'color': this.options.color,
'fontSize': this.options.fontSize,
'textDecoration': this.options.textDecoration
});
}
}
//在插件中使用Beautifier对象
$.fn.myPlugin = function(options) {
//创建Beautifier的实体
var beautifier = new Beautifier(this, options);
//调用其方法
return beautifier.beautify();
}
这样做的好处,也就是上面所阐述的那样。另外还有一个好处就是,自调用匿名函数里面的代码会在第一时间执行,页面准备好过后,上面的代码就将插件准备好了,以方便在后面的代码中使用插件。
目前为止似乎接近完美了。如果再考虑到其他一些因素,比如我们将这段代码放到页面后,前面别人写的代码没有用分号结尾,或者前面的代码将window, undefined等这些系统变量或者关键字修改掉了,正好我们又在自己的代码里面进行了使用,那结果也是不可预测的,这不是 我们想要的。
var foo=function(){
//别人的代码
}//注意这里没有用分号结尾
//开始我们的代码。。。
(function(){
//我们的代码。。
alert('Hello!');
})();
本来别人的代码也正常工作,只是最后定义的那个函数没有用分号结尾而以,然后当页面中引入我们的插件时,报错了,我们的代码无法正常执行。
原因是我们用来充当自调用匿名函数的第一对括号与上面别人定义的函数相连,因为中间没有分号嘛,总之我们的代码无法正常解析了,所以报错。
所以好的做法是我们在代码开头加一个分号,这在任何时候都是一个好的习惯。
var foo=function(){
//别人的代码
}//注意这里没有用分号结尾
//开始我们的代码。。。
;(function(){
//我们的代码。。
alert('Hello!');
})();
同时,将系统变量以参数形式传递到插件内部也是个不错的实践。当我们这样做之后,window等系统变量在插件内部就有了一个局部的引用,可以提高访问速度,会有些许性能的提升,最后我们得到一个非常安全结构良好的代码:
;(function($,window,document,undefined){
//我们的代码。。
//blah blah blah...
})(jQuery,window,document);
而至于这个undefined,稍微有意思一点,为了得到没有被修改的undefined,我们并没有传递这个参数,但却在接收时接收了它,因为实际并没有传,所以‘undefined'那个位置接收到的就是真的'undefined'了。
插件的完整版就是这样了。
;(function($, window, document,undefined) {
//定义Beautifier的构造函数
var Beautifier = function(ele, opt) {
this.$element = ele,
this.defaults = {
'color': 'red',
'fontSize': '12px',
'textDecoration': 'none'
},
this.options = $.extend({}, this.defaults, opt)
}
//定义Beautifier的方法
Beautifier.prototype = {
beautify: function() {
return this.$element.css({
'color': this.options.color,
'fontSize': this.options.fontSize,
'textDecoration': this.options.textDecoration
});
}
}
//在插件中使用Beautifier对象
$.fn.myPlugin = function(options) {
//创建Beautifier的实体
var beautifier = new Beautifier(this, options);
//调用其方法
return beautifier.beautify();
}
})(jQuery, window, document);
现在谈谈关于变量及方法等的命名,没有硬性规定,但为了规范,遵循一些约定还是很有必要的。
变量定义:好的做法是把将要使用的变量名用一个var关键字一并定义在代码开头,变量名间用逗号隔开。
原因有二:
一是便于理解,知道下面的代码会用到哪些变量,同时代码显得整洁且有规律,也方便管理,变量定义与逻辑代码分开。
二是因为JavaScript中所有变量及函数名会自动提升,也称之为JavaScript的Hoist特性,即使你将变量的定义穿插在逻辑代码中,在代码解析运行期间,这些变量的声明还是被提升到了当前作用域最顶端的,所以我们将变量定义在一个作用域的开头是更符合逻辑的一种做法。当然,再次说明这只是一种约定,不是必需的。
变量及函数命名 一般使用驼峰命名法(CamelCase),即首个单词的首字母小写,后面单词首字母大写,比如resultArray,requestAnimationFrame。
对于常量,所有字母采用大写,多个单词用下划线隔开,比如WIDTH=100,BRUSH_COLOR='#00ff00'
。
当变量是jQuery类型时,建议以$开头,开始会不习惯,但经常用了之后会感觉很方便,因为可以很方便地将它与普通变量区别开来,一看到以$开头我们就知道它是jQuery类型可以直接在其身上调用jQuery相关的方法,比如var $element=$('a')
; 之后就可以在后面的代码中很方便地使用它,并且与其他变量容易区分开来。
引号的使用:一般HTML代码里面使用双引号,而在JavaScript中多用单引号。一方面,HTML代码中本来就使用的是双引号,另一方面,在JavaScript中引号中还需要引号的时候,要求我们单双引号间隔着写才是合法的语句,除非你使用转意符那也是可以的。再者,坚持这样的统一可以保持代码风格的一致,不会出现这里字符串用双引号包着,另外的地方就在用单引号。
进行完上面的步骤,已经小有所成了。或许你很早就注意到了,你下载的插件里面,一般都会提供一个压缩的版本一般在文件名里带个'min'字样。也就是minified的意思,压缩浓缩后的版本。并且平时我们使用的jQuery也是官网提供的压缩版本,jquery.min.js。
这里的压缩不是指代码进行功能上的压缩,而是通过将代码里面的变量名,方法函数名等等用更短的名称来替换,并且删除注释(如果有的话)删除代码间的空白及换行所得到的浓缩版本。同时由于代码里面的各种名称都已经被替代,别人无法阅读和分清其逻辑,也起到了混淆代码的作用。
源码经过混淆压缩后,体积大大减小,使代码变得轻量级,同时加快了下载速度,两面加载变快。比如正常jQuery v1.11.0的源码是276kb,而压缩后的版本仅94.1kb!体积减小一半还多。这个体积的减小对于文件下载速度的提升不可小觑。
经过压缩混淆后,代码还能阅读嘛?当然不能,所以顺带还起到了代码保护的作用。当然只是针对你编写了一些比较酷的代码又不想别人抄袭的情况。对于jQuery社区,这里本身就是开源的世界,同时JavaScript这东西其实也没什么实质性方法可以防止别人查看阅读你的代码,毕竟有混淆就有反混淆工具,这里代码压缩更多的还是上面提到的压缩文件的作用,同时一定程度上防止别人抄袭。
工具
这一步不是必需的,但本着把事情做完整的态度,同时你也许也希望有更多人看到或使用你的插件吧。
首先你需要将插件代码放到GitHub上创建一个Service Hook,这样做的目的是你以后更新的插件后,jQuery可以自动去获取新版本的信息然后展示在插件中心的页面上。关于如何传代码到GitHub,你去下载GitHub 提供的客户端工具,就会知道如何操作了,非常方便。关于在GitHub创建Service Hook,也只是点几下而以的事情。
然后需要制作一个JSON格式的清单文件,其中包括关于插件的基本信息,具体格式及参数可以在jQuery官网插件发布指南页面了解到,这里提供一个示例文件。
{
"name": "sliphover",
"title": "SlipHover",
"description": "Apply direction aware 2D/3D hover effect to images",
"keywords": [
"direction-aware",
"animation",
"effect",
"hover",
"image",
"overlay",
"gallery"
],
"version": "1.1.1",
"author": {
"name": "Wayou",
"email": "[email protected]",
"url": "https://github.com/Wayou"
},
"maintainers": [
{
"name": "Wayou",
"email": "[email protected]",
"url": "https://github.com/Wayou"
}
],
"licenses": [
{
"type": "MIT",
"url": "https://github.com/jquery/jquery-color/blob/2.1.2/MIT-LICENSE.txt"
}
],
"bugs": "https://github.com/Wayou/sliphover/issues",
"homepage": "http://wayou.github.io/SlipHover/",
"docs": "http://wayou.github.io/SlipHover/",
"demo":"http://wayou.github.io/SlipHover/",
"download": "https://github.com/Wayou/SlipHover/zipball/master",
"dependencies": {
"jquery": ">=1.5"
}
}
然后就可以在插件的根目录执行现行git代码来发布插件了。其中0.1.0是版本号,以后每次你的插件有新版本发布只需更新上面命令中的版本,创建新的tag,这样jQuery插件中心就会自动获取到新版本信息了
$.fn.myPlugin = function() {
var self = $(this),
instance =self.data('myPlugin');
if(!instance ){
self.data('myPlugin', (instance = new myPlugin()));
}
};
$(selector).data(name)
其中name参数是可选的,规定要取回的数据的名称,如果没有规定的名称,则该方法将以对象的形式从元素中返回所有存储的数据。$(selector).data(name,value)
其中name是必需的,规定要设置的数据的名称。value是必需的,规定要设置的数据的值。一个典型的按键会产生所有这三种事件,依次是keydown,keypress,然后是按键释放时候的key。
在发生keydown和keyup事件时,event对象的keyCode属性中会包含一个代码,与键盘上一个特定的键对应。对于数字字母字符键,keyCode属性的值与ASCII码中对应小写字母或者数字的编码相同。
IE9,以及其他浏览器的event对象都支持一个charCode属性。这个属性只有发生在keypress事件时才包含值,而且这个值是按下的那个键所代表的ASCII编码。
ASCII编码是数字,string.fromCharCode()
该方法可以将编码转换为实际字符。
transitionend 事件在 CSS 完成过渡后触发。
注意: 如果过渡在完成前移除,例如 CSS transition-property 属性被移除,过渡事件将不被触发。
该事件IE10及以上版本才支持。
兼容性:
通过判断某个element的style中是否存在某个css属性。
(function(temp){
if(temp.style['transition'] !== undefined){
return true;
}else{
return false;
}
})(document.createElement('div'));
animate() 方法执行 CSS 属性集的自定义动画。该方法通过 CSS 样式将元素从一个状态改变为另一个状态。CSS属性值是逐渐改变的,这样就可以创建动画效果。只有数字值可创建动画(比如 "margin:30px")。字符串值无法创建动画(比如 "background-color:red")。请使用 "+=" 或 "-=" 来创建相对动画。
语法:(selector).animate({styles},speed,easing,callback)
style 必需。规定产生动画效果的一个或多个 CSS 属性/值。注意: 当与 animate() 方法一起使用时,该属性名称必须是驼峰写法: 您必须使用 paddingLeft 代替 padding-left,marginRight 代替 margin-right.
speed 可选。规定动画的速度。可能的值:毫秒 "slow" "fast"。
easing 可选。规定在动画的不同点中元素的速度。默认值是 "swing"。可能的值:"swing" - 在开头/结尾移动慢,在中间移动快 "linear" - 匀速移动
callback 可选。animate 函数执行完之后,要执行的函数。
看《javascript高级程序设计》的时候,对于JS中获取各种宽度和距离,让我们很容易混淆,还有各个浏览器的兼容问题等等,所以想把他们总结一下。
元素的可见大小由其高度,宽度决定,包括所有内边距、滚动条、和边框大小但是不包括外边距。
其中,offsetTop和offsetLeft属性与包含元素有关,包含元素的引用保存在offsetParent属性中,offsetParent不一定与parentNode的值相等,例如td元素的offsetParent是作为其祖先元素的table元素,
这个是因为table在DOM中是居td元素最近的一个具有大小的元素。
如果想知道某个元素在页面上的偏移量的话,将这个元素的offsetLeft和pffsetTop与其offsetParent的属性相加,如此循环直至根元素。
function getElementLeft(element) {
var actualLeft = element.offsetLeft;
var current = element.offsetParent;
//一直循环offsetParent元素,知道找不到
while(current != null) {
actualLeft += current.offsetLeft;
current = current.offsetParent;
}
return actualLeft;
}
同样的原理,我们也可以球场offsetLeft的值。
注意,所以的这些偏移量属性都是只读的,而且每次访问都需要重新计算。因此应该尽量避免重复访问这些属性,如果需要重复使用这些值的话,可以保存在局部变量中,以提高性能
元素的客户区的大小,指的是元素的内容以及内边距所占据的空间大小。
我们可以看见,客户区的大小就是元素内部的空间大小,因此滚动条占用的空间不计算在内。例如,如何确定浏览器的视口大小?需要考虑到兼容性,在IE7之前的版本,我们需要使用的是document.body,而其他版本的需要使用的是document.documentElement。
function getViewport() {
//如果,浏览器模式是混杂模式的话
if(document.compatMode == 'BackCompat') {
return {
width: document.body.clientWidth,
height: document.body.clientHeight
}
}else {
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
}
}
}
滚动大小:指的的是包含滚动内容的元素大小。有些元素(例如html元素),即使没有执行任何代码也能自动添加滚动条,但是另一些元素,则需要通过css的overflow属性进行设置才能滚动。
width是同高度类似的。
获取匹配元素在当前视口的相对偏移。
返回的对象包含两个整形属性:top 和 left。此方法只对可见元素有效。
获取匹配元素相对父元素的偏移。
返回的对象包含两个整形属性:top 和 left。为精确计算结果,
请在补白、边框和填充属性上使用像素单位。此方法只对可见元素有效。
下图可以对jquery的尺寸的区别。
window对象表示浏览器打开的窗口,window对象是可以省略的。例如window.alert()
可以直接写成'alert()'。
document对象是window对象的一部分,浏览器的html文档称为Document对象。
‘window.document.body’省略为document.body
。
2.window.location和document.location是一样吗?
window对象的location属性引用的是location对象,表示该窗口中当前显示文档的URL。document对象的location对象属性也是引用location对象。
window.location === document.location //true
对于window.inner和outer这两个属性是存在兼容性的问题的,他们仅支持IE9及以上的浏览器。对于兼容问题,后面进行讲解的。
document.body.clientWidth
和document.body.clientHeight
该属性是指元素的可视部分宽度和高度,即padding+content
如果没有出现滚动条,即为元素设定的高度和宽度,如果出现滚动条,滚动条会遮盖元素的宽度,那么改属性就是其本来的宽度减去滚动条的宽高。
.content {
width: 200px;
height: 200px;
background: #ddd;
border:4px solid #666;
overflow: auto;
}
如果内容特别多,出现了滚动条的话,console.log(content.clientWidth); //183
因为width为200px.减去滚动条为17px,则为183px。在Mac下,浏览器的滚动条是不占位的,所以宽度还是200px。
假如无padding和滚动的话,clientWidth = style.width
假如有padding无滚动的话,clientWidth = style.width + style.padding
假如有padding和滚动的话,clientWidth = style.width + style.padding-滚动轴的宽高
document.boby.clientLeft
和document.boby.clientTop
这两个返回是元素周围边框的厚度,如果不指定一个边框或者不定位该元素,那么该值就是0.
这一对属性是用来读取元素的border的宽度和高度的。
clientTop = border-top的border-width;
clientLeft = border-left的border-width;
offsetWidth
和offsetHeight
这一对属性指的是元素的border+padding+content的宽度和高度。
该元素和其内部的内容是否超出元素大小无关,只和本来设定的border以及width和height有关。
假如无padding无滚动条无border: offsetWidth = client.width = style.width
假如有padding无滚动条有border: offsetWidth = style.width+padding+border = clientWidth+border
假如有padding有滚动条有border:offsetWidth = style.width+padding+border = clientWidth+border+滚动条宽。
offsetLeft
和offsetTop
这两个属性。
如果当前元素的父级元素没有进行css定位(position为absolute或者relative),offsetParent为body。
如果当前元素的父级元素中有css定位(position为absolute或者relative)。offsetParent取最近的那个父级元素。
在IE6/7中:offsetLeft = (offsetParent的padding-left)+(当前元素的margin-left)
在IE8及以上和chrome中:offsetLeft = (offsetParent的margin-left)+(offsetParent的borde宽度)+(offsetParent的padding-left)+(当前元素的margin-left)
在FireFox中:offsetLeft = (offsetParent的margin-left)+(offsetParent的padding-left)+(当前元素的margin-left)
body {
border: 20px solid #ccc;
margin: 10px;
padding: 40px;
height: 350px;
width: 500px;
}
.box {
width: 400px;
height: 200px;
padding: 20px;
margin: 10px;
background-color: red;
border: 20px solid #ddd;
}
<body>
<div id="box" class='box'></div>
</body>
window.onload = function(){
var div = document.getElementById("box");
alert(div.offsetLeft);
alert(div.offsetTop);
//chrome80 80
// ie7 50 40
//firefox 60 60
//opera 80 80
}
scrollWidth
和scrollHeight
在document.body和div是有区别的。
body {
border:20px solid #ccc;
margin: 10px;
padding: 40px;
background :#eee;
height: 350px;
width: 500px;
overflow:scroll;
}
alert(document.body.scrollWidth);
alert(document.body.scrollHeight);
在chrome中,document.body的scrollWidth和scrollHeight。
如果body给定宽高小于浏览器的窗口的宽高的话: document.body的scrollWidth通常是浏览器的宽度,
document.body的scrollHeight通常是浏览器的高度。
如果body给定宽高大于浏览器的窗口的宽高的话,且body里面的内容小于body的宽高:document.body的scrollWidth是body给定宽度+其padding+margin+border
document.body的scrollHeight是body给定高度+其padding+margin+border
如果body给定宽高大于浏览器的窗口的宽高的话,且body里面的内容大于body的宽高:
document.body的scrollWidth是内容给定宽度+其padding+margin+border+(body左边一边padding+border+margin)
document.body的scrollHeight是内容给定高度+其padding+margin+border+(body顶部一边padding+border+margin)
对于某个div的scrollWidth和scrollHeight
在firefox中,把body当成了div进行处理的。
scrollLeft和scrollTop,指的是当元素其中的内容超出其高宽的时候,元素被卷起的高度和宽度。
有文档声明(即网页第一句的docType)的情况下,标准浏览器是只认识documentElement.scrollTop的,但chrome在有文档声明时,chrome也只认识document.body.scrollTop.
document.body.scrollTop与document.documentElement.scrollTop两者有个特点,就是同时只会有一个值生效。比如document.body.scrollTop能取到值的时候,document.documentElement.scrollTop就会始终为0;反之亦然。所以,如果要得到网页的真正的scrollTop值,可以这样:
//兼容做法
var scrollLeft = Math.max(document.documentElement.scrollLeft, document.body.scrollLeft);
var scrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop);
//或者
var scrollTop =document.body.scrollTop || document.documentElement.scrollTop;
//或者这样
var scrollTop =document.body.scrollTop+document.documentElement.scrollTop;
一个HTML文档中,文档节点是每一个文档的根节点、文档节点只有一个子节点即html
元素。我们称之为文档元素。文档元素是文档的最外层元素。文档中的其他元素都包含在文档元素中。每个文档只能有一个文档元素。在HTML页面中,文档元素始终都是html
元素,在XML中,没有预定义的元素,因此任何元素都可能成为文档元素。
alert(document.documentElement); //html
alert(document.body); //body
//还是上面那段CSS
alert(document.documentElement.clientHeight);
alert(document.body.clientHeight);
//chrome 974 430
这两者的关系是包含关系。
document.documentElement.clientHeight
是可视区域的大小,’document.body.clientHeight‘是body的宽度+padding为430px。这两个谁大谁小是不确定的。<!DOCTYPE html>
把这个html去掉<!DOCTYPE>
这个两个大小是相同的。document.documentElement.clientHeight
是可视区域的大小所以浏览器窗口可视区域大小,兼容写法
var w= document.documentElement.clientWidth || document.body.clientWidth;
var h= document.documentElement.clientHeight || document.body.clientHeight;
LHS----赋值操作的目标是谁
RHS----谁是赋值操作的源头
例如:下面的这个例子
a = 2;
var a;
console.log(a); //输出2
//对于上面的这段,会进行如下处理
var a;
a = 2;
console.log(2);
//所以会输出2;
console.log(b); //输出undefined
var b = 2;
//对于这段,会进行如下处理
var b;
console.log(b);
b = 2;
//所以控制台会输出undefined的
上面过程,可以比喻成,变量和函数的声明从他们代码中出现的位置被“移动”到最上面,这个过程叫做提升。只有声明本身会被提升,而赋值以及其他逻辑则会留在原地。
foo();
function foo(){
console.log(a); //输出undefined的
var a = 2;
}
//上面这段可以理解为下面这种
foo();
function foo(){
var a;
console.log(a); //所以输出的为undefined的
a = 2;
}
在这里函数foo()会对自身里面的变量a,进行提升,但是并不是提升到整个程序的最上方。
foo(); //TypeError
bar(); //ReferenceError
var foo = function bar() {
//....
};
//这些判断被提升后,实际上回被理解成下面这种形式
var foo;
foo();
bar();
foo = function () {
var bar = self;
//....
}
在这个例子中,foo会被提升,放在最上面,所以foo();输出的不是ReferenceError(一般代表作用域错误),而是TypeError(由于foo存在全局作用域中,而并没有对其赋值,对未定义的值,进行函数的调用会出现非法操作而出错的)。
函数声明以及变量的声明都会被提升。在这里有一个值得注意的细节-----函数的声明会先被提升,然后才是变量。
foo(); //会输出1;
var foo;
function foo(){
console.log(1);
}
foo = function(){
console.log(2);
}
//上面的这段会被引擎理解如下:
function foo(){
console.log(1);
}
//var foo 会因为重复声明被忽略。
foo();
foo = function(){
console.log(2);
}
尽管重复var声明会被忽略,但是后面重复声明的函数还是会覆盖前面的。例如:
foo(); //3
function foo() {
console.log(1);
}
function foo() {
console.log(2);
}
function foo() {
console.log(3);
}
这些说明了一个问题,在同一个作用域进行重复定义是非常糟糕的,而且还会导致各种奇怪的问题。
foo(); //b或者报错 Uncaught TypeError: foo is not a function
var a = true;
if(a){
function foo(){ console.log("a"); }
}else{
function foo(){ console.log("b");}
}
//在不同版chrome浏览器出现两种结果。
所以尽量避免在块内部声明函数。
看一个例子
function foo(){
var a = 2;
function bar(){
console.log(a);
}
return bar;
}
var baz = foo();
baz(); //2
函数 bar() 的词法作用域能够访问 foo() 的内部作用域。然后我们将 bar() 函数本身当作一个值类型进行传递。在这个例子中,我们将 bar 所引用的函数对象本身当作返回值。
在 foo() 执行后,其返回值(也就是内部的 bar() 函数)赋值给变量 baz 并调用 baz() ,实际上只是通过不同的标识符引用调用了内部的函数 bar() 。bar() 显然可以被正常执行。
但是在这个例子中,它在自己定义的词法作用域以外的地方执行。
在 foo() 执行后,通常会期待 foo() 的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去 foo() 的内容不会再被使用,所以很自然地会考虑对其进行回收。
而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是 bar() 本身在使用。
拜 bar() 所声明的位置所赐,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一直存活,以供 bar() 在之后任何时间进行引用。
bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。
因此,在几微秒之后变量 baz 被实际调用(调用内部函数 bar ),不出意料它可以访问定义时的词法作用域,因此它也可以如预期般访问变量 a 。
这个函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。
再看个例子:
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
//当你预期的是每次输出1-5,但是结果却都是输出的6
首先解释 6 是从哪里来的。这个循环的终止条件是 i 不再 <=5 。条件首次成立时 i 的值是6。因此,输出显示的是循环结束时 i 的最终值。
仔细想一下,这好像又是显而易见的,延迟函数的回调会在循环结束时才执行。事实上,当定时器运行时即使每个迭代中执行的是 setTimeout(.., 0) ,所有的回调函数依然是在循环结束后才会被执行,因此会每次输出一个 6 出来。
尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i 。
对上面的那个例子,进行如下改进,就能正常的让工作了。
for (var i=1; i<=5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})();
}
//或者这样
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j ); }, j*1000 );
})( i ); }
}
在迭代内会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。
function foo() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
doSomething();
doAnother();
}
foo();
正如在这段代码中所看到的,这里并没有明显的闭包,只有两个私有数据变量 something和 another ,以及 doSomething() 和 doAnother() 两个内部函数,它们的词法作用域(而这就是闭包)也就是 foo() 的内部作用域。
看下面的这个例子
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
//返回了一个对象,分别有两个方法,即这个模块定义的两个函数
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
这个模式在 JavaScript 中被称为模块。最常见的实现模块模式的方法通常被称为模块暴露
首先, CoolModule() 只是一个函数,必须要通过调用它来创建一个模块实例。如果不执行外部函数,内部作用域和闭包都无法被创建。
其次, CoolModule() 返回一个用对象字面量语法 { key: value, ... } 来表示的对象。这个返回的对象中含有对内部函数而不是内部数据变量的引用。我们保持内部数据变量是隐藏且私有的状态。可以将这个对象类型的返回值看作本质上是模块的公共 API。
这个对象类型的返回值最终被赋值给外部的变量 foo ,然后就可以通过它来访问 API 中的属性方法,比如 foo.doSomething() 。
从模块中返回一个实际的对象并不是必须的,也可以直接返回一个内部函数。jQuery 就是一个很好的例子。 jQuery 和 $ 标识符就是 jQuery 模块的公共 API,但它们本身都是函数(由于函数也是对象,它们本身也可以拥有属性)。
如果要更简单的描述,模块模式需要具备两个必要条件。
当只需要一个实例时,可以对这个模式进行简单的改进来实现单例模式:
var foo = (function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
})();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
function CoolModule(id) {
function identify() {
console.log( id );
}
return {
identify: identify
};
}
var foo1 = CoolModule( "foo 1" );
var foo2 = CoolModule( "foo 2" );
foo1.identify(); // "foo 1"
foo2.identify(); // "foo 2"
var foo = (function CoolModule(id) {
function change() {
// 修改公共 API
publicAPI.identify = identify2;
}
function identify1() {
console.log( id );
}
function identify2() {
console.log( id.toUpperCase() );
}
var publicAPI = {
change: change,
identify: identify1
};
return publicAPI;
})( "foo module" );
foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE
通过在模块实例的内部保留对公共 API 对象的内部引用,可以从内部对模块实例进行修改,包括添加或删除方法和属性,以及修改它们的值。
大多数模块依赖加载器 / 管理器本质上都是将这种模块定义封装进一个友好的 API。
var MyModules = (function Manager() {
var modules = {};
function define(name, deps, impl) {
for (var i=0; i<deps.length; i++) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply( impl, deps );
}
function get(name) {
return modules[name];
}
return {
define: define,
get: get
};
})();
这段代码的核心是 modules[name] = impl.apply(impl, deps) 。为了模块的定义引入了包装函数(可以传入任何依赖),并且将返回值,也就是模块的 API,储存在一个根据名字来管理的模块列表中
MyModules.define( "bar", [], function() {
function hello(who) {
return "Let me introduce: " + who;
}
return {
hello: hello
};
} );
MyModules.define( "foo", ["bar"], function(bar) {
var hungry = "hippo";
function awesome() {
console.log( bar.hello( hungry ).toUpperCase() );
}
return {
awesome: awesome
};
} );
var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );
console.log(
bar.hello( "hippo" )
); // Let me introduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO
函数在代码中被调用的位置,而不是函数声明的位置。只有仔细分析这个调用位置才能知道this代表什么。
寻找调用位置就是寻找“函数被调用的位置”,在这里最重要的是分析调用栈(就是为了到达当前执行位置所调用的所有函数)----我们关心的是调用位置就在当前正在执行的函数的前一个调用中。
function baz(){
//当前调用栈是:baz
//因此,当前调用位置是全局作用域
console.log("baz");
bar(); //bar的调用位置
}
function bar(){
//当前调用栈是:baz-->bar
//因此,当前调用位置是baz
console.log("bar");
bar(); //foo的调用位置
}
function foo(){
//当前调用栈是:baz-->bar-->foo
//因此,当前调用位置bar
console.log("bar");
bar(); //foo的调用位置
}
baz(); //baz的调用位置
必须找到调用位置,然后判断需要应用下面四条规则中的那一条。
最常用的函数调用类型,独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。
function foo(){
console.log(this.a);
}
var a= 2;
foo(): //2
我们分析一下:调用位置实全局作用域被调用的。所以foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定。
如果使用严格模式,那么全局对象将无法使用默认绑定,因此this会绑定到undefined
function foo(){
"use strict"; //注意不要代码中混合使用严格和非严格模式
console.log(this.a);
}
var a= 2;
foo(): //TyoeError: this is undefined
在这里需要注意一点:虽然this的绑定规则完全取决于调用位置。但是只有foo()运作在非严格模式下,默认绑定到全局对象,严格模式下与foo()的调用位置无关。
function foo(){
// foo()运作在非严格模式下,默认绑定到全局对象
console.log(this.a);
}
var a= 2;
(function(){
"use strict";
// 严格模式下与foo()的调用位置无关。
foo(); //2
})();
function foo(){
console.log(this.a);
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); //42
对象属性引用链中只有最顶层或者最后一层会影响调用位置。
1 -------------隐式丢失
一个最常见的this绑定问题就是隐式绑定的函数会丢失绑定对象,页就是说它会应用默认绑定。从而把this绑定到全局对象或者undefined上,取决于是否严格模式。
function foo(){
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; //函数的别名
var a = "oops, global" //a是全局对象的属性
bar(); //oops, global
虽然bar是obj.foo的一个引用,但实际上,它引用的是foo函数的本身,因此此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
function foo(){
console.log(this.a);
}
function doFoo(fn){
//fn其实引用的是foo
fn(); //调用位置
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global" //a是全局对象的属性
doFoo(obj.foo); // oops, global
参数传递其实就是一种隐式赋值,因此我们传人函数时也会被隐式赋值,所以结果和上一个例子一样。
function foo(){
console.log(this.a);
}
var obj = {
a:2
};
foo.call(obj); //2
通过foo.call(...),我们可以在调用foo时,强制把它的this绑定到obj上
1 -------------硬绑定
function foo(){
console.log(this.a);
}
var obj = {
a: 2
};
var bar = function(){
foo.call(obj);
};
bar(): //2
//硬绑定的bar不可能再修改它的this
bar.call(window) //2
硬绑定的典型应用场景就是创建一个包裹函数,传入所有的参数并返回接受到的所有值
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
var obj = {
a:2
};
var bar = function(){
return foo.apply(obj, arguments);
};
var b = bar(3); //2 3
console.log(b) //5
在ES5中提供了内置的方法Function.prototype.bind, 其用法如下:
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
var obj = {
a:2
};
var bar = foo.bind(obj);
var b = bar(3); //2 3
console.log(b); //5
bind(...)会返回一个硬编码的新函数,它会把参数设置为this的上下文并调用原始函数。
2 -------------API调用的“上下文”
function foo(el){
console.log(el, this.id);
}
var obj = {
id: "awesome"
};
[1, 2 ,3].forEach(foo, obj);
//forEach除了接受一个必须的回调函数参数,还可以接受一个可选的上下文参数(改变回调函数
// 里面的this指向)
//1 awesome 2 awesome 3 awesome
function Foo(a)[
this.a = a;
}
var bar = new Foo(2);
console.log(bar.a); //2
看显式绑定和隐式绑定谁的优先级更高?
function foo(){
console.log(this.a);
}
var obj1 = {
a:2,
foo: foo
};
var obj2 = {
a:3,
foo: foo
};
obj1.foo(); //2
obj2.foo(); //3
obj1.foo.call(obj2); //3
obj2.foo.call(obj1); //2
看new绑定和隐式绑定谁的优先级更高?
function foo(something) {
this.a = something;
}
var obj1 = {
foo: foo
}
var obj2 = { };
obj1.foo(2);
console.log(obj1.a); //2
obj1.foo.call(obj2, 3);
console.log(obj2.a); //3
var bar = new obj1.foo(4);
console.log(obj1.a); //2
console.log(bar.a); //4
可以看出new绑定比隐式绑定的优先级高 。
那么硬绑定与new绑定的优先级谁更高?
function foo(something){
this.a = something
}
var obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); //2
var baz = new bar(3);
console.log(obj1.a); //2
console.log(baz.a); //3
从上面,我们可以看出来,bar被硬绑定到obj1上,但是new bar(3) 并没有像我们预计的那样把obj1.a修改为3,相反,new修改了硬绑定(到obj1的)调用bar(...)中的this,因为使用了new绑定,我们得到了一个名字为baz的新对象,并且baz.a的值为3。new绑定优先级更高。
如果你把null或者undefined作为this的绑定对象传入call apply或者bind, 这些值调用的时会被忽略,实际应用的是默认绑定规则:
function foo(){
console.log(this.a);
}
var a =2;
foo.call(null); //2
什么情况下会传入null呢?
function foo(a, b) {
console.log("a:"+a+", b:"+ b);
}
//把数组“展开”称参数
foo.apply(null, [2,3]); //a:2,b:3
//使用bind()进行柯里化
var bar = foo.bind(null, 2);
bar(3); //a:2, b:3
如果函数并不关心this的话,你仍然需要传入一个占位符,这是null可能是一个不错的选择。
有可能创建一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则。间接引用最容易在赋值时发生。
function foo(){
console.log(this.a);
}
var a =2;
var o = {a: 3, foo: foo};
var p = {a: 4};
o.foo(); //3
(p.foo = o.foo)() ; //2
赋值表达式p.foo = o.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo(),
根据之前的规则,这里会应用默认绑定。
注意:对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this会被绑定undefined,否则this会绑定到全局对象。
JSON是一种数据格式,不是一种编程语言,并不是只用JavaScript才使用JSON,毕竟JSON只是一种数据格式,很多编程语言都有针对JSON的解析器和序列化器。
其语法可以表示以下三种类型
使用与JavaScript相同的语法,可以在JSON中表示字符串,数值,布尔值,null。但JSON不支持JavaScript中的undefined。
5, "hello world"
JavaScript字符串与JSON字符串最大的区别为:JSON字符串必须使用双引号(单引号会导致语法错误)
对象作为一种复杂数据类型,表示的是一组无序的键值对儿,而每个键值对儿的值可以简单值,也可以复杂数据类型的值。
//javascript的对象字面量
var person = {
name: "jerry",
age: 29
};
//JSON表示上述对象的方式如下
{
"name": "jerry",
"age": 29
}
于JavaScript的对象字面量相比,JSON对象有两个区别:
看个例子:
{
"name": "jerry",
"age": 29,
"school": {
"name": "dom",
"location": "am"
}
}
在这个例子,虽然有两个“name”属性,但由于他们分别属于不同的对象,因此这样是完全没有问题的,
不过,同一个对象绝对不应该出现两个同名属性。
数组也是一种复杂数据类型,表示一组有序的值的列表,可以通过数值的索引来访问其中的值,数组的值也可以是任意类型:简单值,对象或数组。
注意JSON的数组,没有变量和分号。把数组和对象结合起来,可以构成更复杂的数据集合。
[
{
"title": "professional javascript",
"authors": [
"nicholas c.zakas"
],
"edition": 3,
"year": 2009
},
{
"title": "professional javascript",
"authors": [
"nicholas c.zakas"
],
"edition": 3,
"year": 2009
}
]
ECMAScript中对JSON行为进行规范,定义了全局对象JSON对象。分别有两个方法。stringify()和parse()
stringify()方法:把JavaScript对象序列化为JSON字符串。
看个例子:
var book = {
title: "professional javascript",
authors: [
"nicholas c.zakas"
],
edition: 3,
year: 2009
};
var jsonText = JSON.stringify(book);
console.log(jsonText);
//输出为{"title":"professional javascript","authors":["nicholas c.zakas"],"edition":3,"year":2009}
把JSON字符串解析为原生的JavaScript值。
//还是上面的那个例子
var bookCopy = JSON.parse(jsonText);
在这里,需要注意虽然book和bookCopy具有相同的属性,但他们是两个独立的,没有任何关系的对象。如果给方法传递的不是有效的JSON,那么该方法胡抛出错误。
在上面JSON.stringify()方法,除了要序列化的JavaScript对象外,还可以接受另外两个参数,这两个参数用于指定以不同的方式序列化JavaScript对象,第一个参数是一个过滤器,可以是数组,也可以是一个函数,第二个参数是一个选项,表示是否在JSON字符串中保留缩进,可以单独使用,或者组合使用这两个
参数。
如果过滤参数是数组,那么JSON.stringify()的结果中将只包含数组中列出来的属性,看一个例子
var book = {
title: "professional javascript",
authors: [
"nicholas c.zakas"
],
edition: 3,
year: 2009
};
var jsonText =JSON.stringify(book, ["title", "edition"]);
console.log(jsonText);
//输出的结果为{"title":"professional javascript","edition":3}
如果过滤参数是对象的话,那么情况就会有些不同,函数接受两个参数,属性名(键)和属性值。
根据属性名可以知道应该如何处理要序列化对象中的属性。属性名只能是字符串,而值并非键值对儿结构的值时,键名可以是空字符串。
为了改变序列化的值,函数返回的值就是相应键的值。
var book = {
title: "professional javascript",
authors: [
"nicholas c.zakas"
],
edition: 3,
year: 2009
};
var jsonText = JSON.stringify(book, function(key, value){
switch(key) {
case "authors":
return value.join(",")
case "year":
return 5000;
case "edition":
return undefined;
//一定要加上default默认值。以便其它值都能出现在结果中。
default:
return value;
}
});
console.log(jsonText);
//输出的结果为{"title":"professional javascript","authors":"nicholas c.zakas","year":5000}
该方法的第三个参数用于控制结果中缩进和空白符,如果该参数是一个数值,那么它表示每个级别的缩进的空格数。
例如:每个级别缩进4个空格。
var book = {
title: "professional javascript",
authors: [
"nicholas c.zakas"
],
edition: 3,
year: 2009
};
var jsonText = JSON.stringify(book, null, 4);
console.log(jsonText);
/*
其结果为:
{
"title": "professional javascript",
"authors": [
"nicholas c.zakas"
],
"edition": 3,
"year": 2009
}
*/
在JSON.stringify()的方法中,也在结果中插入了换行符以提高了可读性,只要传入有效的控制缩进的参数值,结果字符串就会包含换行符(只缩进而不换行的意义不大)最大缩进空格数为10,所有大于10的值都会自动转换为10。
如果缩进参数是一个字符串而不是一个数值的话,则这个字符串在JSON字符串中被用作缩进字符(不再使用空格),在使用字符串的情况下,可以将缩进字符设置为制表符,或者两个短划线之类的任意字符。缩进字符串最长不超过10个字符长,如果字符串超过了10个,结果中将只出现前10个字符。
var book = {
title: "professional javascript",
authors: [
"nicholas c.zakas"
],
edition: 3,
year: 2009
};
var jsonText = JSON.stringify(book, null, "- -");
console.log(jsonText);
/*
其结果为:
{
- -"title": "professional javascript",
- -"authors": [
- -- -"nicholas c.zakas"
- -],
- -"edition": 3,
- -"year": 2009
}
*/
toJSON()方法是为了满足某些对象进行自定义序列化的需求而出现的。
原生的Date对象有一个toJSON()的方法,能够将JavaScript的Date对象自动转换成ISO 8601日期字符串(与在Date对象上调用toISOSting()的结果完全一样)
可以为任何对象添加toJSON()方法。例如:
var book = {
title: "professional javascript",
authors: [
"nicholas c.zakas"
],
edition: 3,
year: 2009,
toJSON: function(){
this.title;
}
};
var jsonText = JSON.stringify(book);
console.log(jsonText);
//返回的结果为"professional javascript"
toJSON()方法可以返回任何值,它都能正常工作,比如让这个方法返回undefined,此时如果包含他的对象嵌入到另一个对象中,会导致他的值变为null,而如果他是顶级对象的话,其结果就是undefined的。
toJSON()方法可以作为函数过滤器的补充,因此理解序列化的内存顺序十分重要,假设把一个对象传入JSON.stringify()中,序列化该对象的顺序如下:
JSON.parse()方法也可以接受另一个参数。该参数是一个函数,将在每一个键值对儿上调用。为了区别JSON.stringify()的接替(过滤)函数,这个函数被称为还原函数。但实际上这两个函数的签名是相同的----他们都接受两个参数,一个是键一个是值。而且都需要返回一个值。
如果还原函数返回undefined,则表示要从结果中删除相应的值。如果返回其他值,则将该值插入结果中。
一直都Ajax都比较模糊。就了解一些简单的。但是又觉得好复杂,一点都不想知道,所以想到了一个方法,边看书,边把每一个字都打出来。这样就会强迫自己学下来。这个方法还是很管用的。发现ajax还是不是那么不可理解。慢慢有点理解了。
FormData类型为序列化表单以及创建表单格式相同的数据(用于通过XHR传输)提供了便利。
看一个例子
var data = new FormData();
data.append('name', 'nico');
append方法,接受两个参数,键和值。分别对应表单字段的名字和字段中包含的值。可以添加任意多个键值对。而通过向FormData构造函数中传入表单元素,也可以用表单元素的数据预先向其中填入键值对。
var data = new FormData(document.form[0]);
创建了FormData的实例后,可以将它直接传给XHR的send方法。例如
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if(xhr.readyState == 4 ){
if ((xhr.status >= 200 && xhr< 300) || xhr.status == 304) {
console.log(xhr.responseText);
}else{
console.log("Request was unsuccessful: " + xhr.status);
}
}
};
xhr.open('post', 'example.php', true);
var form = document.getElementById('user-info');
xhr.send(new FormData(form));
使用FormData的方便在于,不必明确的在XHR对象上设置请求头部。XHR对象能够识别传入的数据类型是FormData的实例。
XHR对象添加了一个timeout的属性,表示请求在等待响应多少毫秒之后就终止。在给timeout设置一个数组后,如果在规定的时间内浏览器还没有接收到响应,那么就会触发timeout事件,进而会调用ontimeout事件处理程序。
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if(xhr.readyState == 4 ){
try {
if ((xhr.status >= 200 && xhr< 300) || xhr.status == 304) {
console.log(xhr.responseText);
}else{
console.log("Request was unsuccessful: " + xhr.status);
}
} catch(ex) {
//由ontimeout事件处理程序处理
}
}
};
xhr.open('get', 'timeout.php', true);
xhr.timeout = 1000;
//将超时设置为1秒,仅适用IE8+
xhr.ontimeout = function() {
console.log('request did not return in a second');
xhr.send(null);
}
在这个例子中,表示如果请求在1秒钟内没有返回,就自动终止。请求终止时,会调用ontimeout事件处理程序,但此时readyState可能已经改变为4,这意味着会调用onreadystatechange事件处理程序,可是,如果在超时终止请求之后再访问status属性,就会导致错误。为了避免浏览器的错误报告,可以将检查status属性的语句封装在一个try-catch语句中。
该方法用于重写XHR响应的MIME类型。例如,服务器返回的MIME类型是text/plain,但数据中实际包含的是XML。根据MIME类型,即使数据是XML,responseXML属性中仍然是null,通过调用该方法,可以保证把响应当做XML而非纯文本来处理。
var xhr = new XMLHttpRequest();
xhr.open('get', 'text.php', true);
xhr.overrideMimeType('text/xml');
xhr.send(null);
在这里,强迫把XHR对象将响应当做XML而非纯文本来处理。调用overrideMimeType()必须在send()方法之前。才能保证重写响应的MIME类型。
Progress Events规范是W3C的一个工作草案,定义了与客户端服务器通信有关的事件。这些事件最早其实只针对XHR操作,但目前也被其他API借鉴。有以下6个事件。
每一个请求都是从loadstart事件开始,接下来是一个或者多个progress事件,然后触发error,abort,或者load事件中的一个,最后触发loaded事件结束。
load事件,用于代替onreadystatechange事件。响应接受完毕后触发load事件,因此没有必要去检查readyState属性。而onload事件处理程序会接受到一个event对象,其target属性就指向XHR对象实例。
因此可以访问XHR对象的所有方法和属性。但是并非所有浏览器都为这个事件实现了适当的事件对象。
所有开发人员不得不像下面这样被迫使用XHR对象变量。
var xhr = new XMLHttpRequest();
xhr.onload = function() {
if((xhr,status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
}else{
alert("Request was unsuccessful: "+ xhr.status);
}
};
xhr.open('get', 'example.php', true);
xhr.send(null);
只有浏览器接受到服务器的响应,不管其状态如何,都会触发load事件。而这意味着必须要检查status属性,才能确定数据是否真的已经可用。
这个事件会在浏览器接收新数据期间周期性地触发。而onprogress事件处理程序会接受到一个event对象,其target属性是XHR对象,但包含着三个额外的属性:lengthComputable、position和totalSize.其中,lengthComputable是一个表示进度是否可用的布尔值,position表示已经接收的字节数,totalSize表示根据Content-Length响应头部确定的预期字节数。
这样就可以给用户创建一个进度指示器。
var xhr = new XMLHttpRequest();
xhr.onload = function() {
if((xhr,status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
}else{
alert("Request was unsuccessful: "+ xhr.status);
}
};
xhr.onprogress = function(event) {
var divStatus = document.getElementById('status');
if (event.lengthComputable){
divStatus.innerHTML = 'Received' + event.position + ' of ' + event.totalSize + 'bytes';
}
};
xhr.open('get', 'example.php', true);
xhr.send(null);
为了确保正常执行,必须在调用open()方法之前添加onprogress事件处理程序。
定义了在必须访问跨源资源时,浏览器与服务器之间该如何沟通。CORS基本的**是:就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或者响应是该成功还是失败。
例如,一个简单的使用GET或者POST发送的请求,它没有自定义的请求头部,而主体内容是text/plain,在发送该请求时,需要给它附加一个额外的Origin头部,其中包含请求页面的源信息(协议,域名,端口)以便服务器根据这个头部信息来决定是否给予响应
Origin: http://www.xxx.net
Origin头部的一个示例
如果服务器认为这个请求可以接受,就在Access-Control-Allow-Origin头部中回发相同的源信息(如果是公共资源,可以回发‘*’)
Access-Control-Allow-Origin: http://www.xxx.net
如果没有这个头部,或者这个头部信息不匹配,浏览器就会驳回请求。注意,请求和响应都不包含cookie信息。
在IE8中引入了XDR(XDomainRequest)类型,这个对象与XHR类似,但是能实现安全可靠的跨域通信。
它与XHR的区别在于:
被请求的资源可以根据它认为合适的任意数据(用户代理,源页面)来决定是否设置Access-Control-Allow-Origin头部,作为请求的一部分,Origin头部的值表示请求的来源域,以便远程资源明确地识别XDR请求。
XDR对象的使用方法:
var xdr = new XDomainRequest();
xdr.onload = function() {
alert(xdr.responseText);
};
xdr.open('get', 'http://www.xx.net/page/');
xdr.send(null);
除了错误本身之外,没有其他可用的信息可用,因此唯一能够确定的就只要请求为成功。由于导致XDR请求失败的因素很多,因此可以通过onerror事件处理程序来捕获该事件,否则,即使请求失败也不会有任何提示。
var xdr = new XDomainRequest();
xdr.onload = function() {
alert(xdr.responseText);
};
xdr.onerror = function() {
alert("an error occurred.");
}
xdr.open('get', 'http://www.xx.net/page/');
xdr.send(null);
在请求返回之前可以调用abort()方法来终止请求
xdr.abort()
;
与XHR一样,XDR对象也支持timeout属性以及ontimeout事件处理程序。
var xdr = new XDomainRequest();
xdr.onload = function() {
alert(xdr.responseText);
};
xdr.onerror = function() {
alert("an error occurred.");
}
xdr.timeout = 1000;
xdr.ontimeout = function() {
alert("request took too long");
}
xdr.open('get', 'http://www.xx.net/page/');
xdr.send(null);
为了支持POST请求,XDR对象提供了contentType属性,用来表示发送数据的格式。
var xdr = new XDomainRequest();
xdr.onload = function() {
alert(xdr.responseText);
};
xdr.onerror = function() {
alert("an error occurred.");
}
xdr.open('get', 'http://www.xx.net/page/');
xdr.contentType = "application/x-www-form-urlencoded";
xdr.send(null);
通过XMLHttpRequest对象实现了对CORS的原生支持。在尝试打开不同来源的资源时,无需额外编写代码,就可以触发这个行为。要请求位于另一个域中的资源,使用标准的XHR对象并在open()方法中传入绝对URL即可。
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if(xhr.readyState == 4 ){
if ((xhr.status >= 200 && xhr< 300) || xhr.status == 304) {
console.log(xhr.responseText);
}else{
console.log("Request was unsuccessful: " + xhr.status);
}
}
};
xhr.open('get', 'http://www.xx.net/page', true);
xhr.send(null);
与IE中XDR对象不同的是,通过跨域XHR对象可以访问status和statusText属性,而且还支持同步请求,跨域XHR对象也有一些限制。
由于无论同源请求还是跨源请求都支持相同的接口,因此本地资源,最好使用相对URL,在访问远程资源再使用绝对URL,这样能消除歧义,避免出现访问限制头部或者本地cookie信息等问题。
CORS通过一种叫Preflighted Requests的透明服务器验证机制支持开发人员使用自定义的头部、GET或POST之外的方法,以及不同类型的主体内容。在使用下列高级选项来发送请求时,就会向服务器发送一个Preflight请求,这种请求使用OPTIONS方法。发送下列头部。
例如:一个带有自定义头部NCZ的使用的POST方法发送的请求
Origin: http://www.nczonline.net Access-Control-Request-Method: POST Access-Control-Request-Headers:NCZ
发送这个请求后,服务器可以决定是否允许这种类型的请求。服务器通过在响应中发送如下头部与浏览器进行沟通。
例如:
Access-Control-Request-Origin: http://www.nczonline.net Access-Control-Request-Method: POST, GET Access-Control-Request-Headers: NCZ Access-Control-Max-Age1728000
Preflight 请求结束后,结果将按照响应中指定的时间缓存起来,而为此付出的代价只是第一次发送这种请求时会多一次HTTP请求。IE10及更早的版本都不支持。
默认情况下,跨源请求不提供凭据(cookie, HTTP认证及客户端SSL证明等)。通过将withCredentials属性设置为true。可以指定某个请求应该发送的凭据。如果服务器接受带凭据的请求,会用下面的HTTP头部来响应。
Access-Control-Allow-Credentials: true
如果发送的是带凭据的请求,但是服务器的响应中没有包含这个头部,那么浏览器就不会把响应交给javascript(于是,responseText中将是空字符串,status状态为0,而且会调用onerror()事件处理程序),另外,服务器还可以在Preflight响应中发送这个HTTP头部。表示允许源发送带凭据的请求。
检测XHR是否支持CORS的最简单方式,就是检查是否存在withCredentials属性,再结合检测XDomainRequest对象是否存在。就可以兼顾所有浏览器了。
function createCORSRequest(method, url) {
var xhr = new XMLHttpRequest();
if('withCredentials' in xhr) {
// 此时即支持CORS的情况
// 检查XMLHttpRequest对象是否有“withCredentials”属性
// “withCredentials”仅存在于XMLHTTPRequest2对象里
xhr.open(method, url, true);
}else if (typeof XDomainRequest != 'undefined'){
// 否则检查是否支持XDomainRequest,IE8和IE9支持
// XDomainRequest仅存在于IE中,是IE用于支持CORS请求的方式
xhr = new XDomainRequest();
xhr.open(method, url);
}else {
// 否则,浏览器不支持CORS
xhr = null;
}
return xhr;
}
var request = createCORSRequest('get', 'http://www.xxx.net');
if(request ) {
request.onload = function() {
//对request.responseText进行处理
}
request.send();
}esle {
throw new Error('CORS not supported');
}
Firefox、Safari、Chrome中的XMLHttpRequest对象与IE中的XDomainRequest对象类似,都提供够用的接口,因此以上模式还是相当有用的。这两个对象共同的属性和方法如下:
记得有一次,想随机生成一个颜色值,但是总是有一些小错误。从这篇文章学到挺多的知识的。所以想记录下来。
Addy Osmani的一行代码 ,这行代码对于你调试你的CSS是很有用的。尝试在你浏览器的Console(F12)中运行一下,你会发现页面被不同的颜色块高亮了,这个方法非常简单,但是你自己写的话可能产生非常多的代码,让我们来研究一下它是怎么实现的。
[].forEach.call($$("*"),function(a){
a.style.outline="1px solid #"+(~~(Math.random()*(1<<24))).toString(16)
})
我们首先需要选择页面上的所有元素。Addy使用了只能在console调试工具中使用的$$函数,你可以在console中输入$$('a')自己试一下。它会返回当前页面的所有anchor(链接)元素。
$$与document.querySelectorAll是等价的。所以$$('')与 document.querySelectorAll('')是等价的。
对于我来说,$$的使用已经让我学到了很多。但是选择页面上的所有元素的知识还有很多。Mathias Bynens就在评论中指出document.all 其实也能选取选用元素,而且兼容所有主流浏览器。
现在我们有了一个所有元素的节点列表(NodeList),现在我们想遍历它们,并给他们加上有颜色的边框。我们先看看能从这行代码里发现什么:
[].forEach.call( $$('*'), function( element ) { /* And the modification code here */ });
NodeList看起来像一个Array(数组),你可以使用中括号来访问他们的节点,而且你还可以通过length属性知道它有多少元素。但是它并没有实现Array的所有接口,因此使用 $$('*').forEach 会返回错误,在JavaScript的世界里,有一堆看起来像Array但其实不是的对象。如function中的arguments对象。因此在他们身上通过call和apply来应用数组的方法是非常有用的。例如下面的这个例子。
function say(name) {
console.log(this); // String {0: "h", 1: "o", 2: "l", 3: "a", length: 4, [[PrimitiveValue]]: "hola"}
//this就是字符串'hola'
console.log( this + ' ' + name );
}
say.call( 'hola', 'Mike' );
// 打印输出 'hola Mike'
之前的一行代码使用 [].forEach.call 代替 Array.prototype.forEach.call 减少了代码的编写量 ( 另外一个很有意思的地方 );如果$$('')返回是个数组的话,它与$$('').forEach是等价的。
让元素有一个漂亮的边框,这行代码使用了CSS的outline属性。有一点你可能不知道,在CSS渲染的盒子模型(Box Model)中,outline并不会改变元素及其布局的位置。因此这比使用border属性要好得多,所以这一部分其实并不难理解
a.style.outline="1px solid #" + color
怎样定义颜色值其实是比较有意思的
(~~(Math.random()*(1<<24))).toString(16)
我们想构造的其实是一个16进制的颜色值,像白色FFFFFF,蓝色0000FF等等。
首先我们学到了可以使用数字类型的toString方法进行十进制到16进制的转换。
其实你可以用它进行任意进制的转换
(30).toString(); // "30"
(30).toString(10); // "30"
(30).toString(16); // "1e" 16进制
(30).toString(2); // "11110" 二进制
(30).toString(36); // "u" 36 是最大允许的进制
因此16进制中的ffffff其实是 parseInt("ffffff", 16) == 16777215,16777215是2^24 - 1的值
因此左位移操作乘以一个随机数 Math.random()*(1<<24) 可以得到一个0 到 16777216之间的值
此时把1想左边移动24位,其值为2^24。
但是还不够,Math.random返回的是一个浮点数字,我们只需要整数部,这里使用了“~”操作符(按位取反操作)。
这行代码并不关心正负值。因此通过两次取返就可以得到纯整数部,我们还可以将~~视为parseInt的简写:
var a = 12.34, // ~~a = 12
b = -1231.8754, // ~~b = -1231
c = 3213.000001 // ~~c = 3213
;
~~a == parseInt(a, 10); // true
~~b == parseInt(b, 10); // true
~~c == parseInt(c, 10); // true
按位或 "|"操作符也可以得到相同的结果
~~a == 0|a == parseInt(a, 10)
~~b == 0|b == parseInt(b, 10)
~~c == 0|c == parseInt(c, 10)
我们最终得到了一个 0 到 16777216之间的随机数,然后使用toString(16)转换成16进制,它就是这样工作的。
有3个函数可以把fie数值转换为数值:
如果是Boolean值,true和false将分别被转换为1和0
如果是数字值,只是简单的传入和返回
如果是null值,返回0
如果是undefined,返回NaN
如果是字符串的话。
如果字符串中包含数字(包括前面带正号和负号的情况),则将其转换为十进制数值,即'1'会变成1,
而'011'会变成11(注意,前导的零被忽略了)。
如果字符串中包含有效的浮点格式,如'1.1'则将其转换为对应的浮点数值(注意,前导的零被忽略了)。
如果字符串中包含有效的十六进制,例如'0xf',则将其转换为相同大小的十进制整数数值。
如果字符串是空的(不包含任何字符),则将其转换为0
如果是对象,则调用对象的valueOf()方法,然后依照前面的规则转换返回的值。如果转换的结果是NaN
则调用对象的toString()方法,然后再次依次依照前面的规则转换返回的字符串值。
由于Number函数在转换字符串时比较复杂而且不够合理,因此在处理整数的时候更常用的是parseInt函数,该函数在转换字符串时,更多的是看其是否符合数值模式。
它会忽略字符串前面的空格。直到找到第一个非空格字符。
如果第一字符不是数字字符或者负号,parseInt会返回NaN
对空字符串返回0
如果第一个字符是数字字符,parseInt会继续解析第二字符,直到解析完所有后续字符或者遇到一个非数字字符。
如果字符串中的第一个字符是数字字符,parseInt()也能够识别各种整数格式(十进制,八进制,十六进制)即如果字符串以'0x'开头且后跟着数字字符,就会将其当做一个十六进制整数,如果字符串以'0'开头且后跟着数字字符,则会将其当做一个八进制来解析。
var num = parseInt(22.5);
console.log(num); //22 因为小数点并不是有效的数字字符
ECMAScript 3 JavaScript和ECMAScript 5 JavaScript 在进行八进制字面量的字符串解析时,是存在分歧的。parseInt('070')对于这个前者认识是56,即八进制,而后者认为70,即十进制。
ECMAScript 5 JavaScript 已经不具有解析八进制的能力。
为了消除在使用parseInt函数的上述困惑,可以为这个函数提供第二个参数,转换时使用的基数(即多少进制)。
var num = parseInt('0xAF',16); //175
var num1 = parseInt('AF', 16); //175
//在指定16作为第二个参数,字符串可以不带前面的0x
parseFloat与parseInt函数类型,也是第一个字符开始解析每个字符,而且也是一直解析到字符串末尾。
或者解析遇到一个无效的浮点数字字符为止。也就是说字符串的第一个小数点是有效的。而第二个小数点是无效的。因此它后面的字符串将被忽略。
var num = parseFloat('22.24.5'); //22.24
而且parseFloat这个方法只解析十进制的值。因此它没有用第二个参数指定基数的用法。
在多数情况下,该方法不必传递参数。但是,在调用数值的toString()方法时,可以传递一个参数,输出数值的基数。默认情况下,toString()方法以十进制格式返回数值的字符串表示。该方法可以输出二进制,八进制,十六进制,乃至其他任意有效进制格式表示的字符串值。
var num = 10;
console.log(num.toString()); //'10'
console.log(num.toString(2)); //'1010'
位操作符,即按内存中表示数值的位来操作数值。
var num1 = 25;
var num2 = ~num1;
console.log(num2); //-26
最进,在学习一个文字滚动效果的时候,在用setTimeout,总是出现作用域的问题,搜了好久,终于在一遍博文中发现了问题的所在。原文链接
setTimeout()方法,接受两个参数,要执行的代码和以毫秒表示的时间。其中第一个参数可以是一个包含javascript代码的字符串,也可以是一个函数。
//不建议传递字符串
setTimeout("alert('hello world')", 1000);
//推荐的调用方式
setTimeout(function(){
alert('hello world');
},1000)
因为传递字符串可能导致性能损失。因此不建议以字符串作为第一个参数。第二个参数是一个表示等待多长时间的毫秒数,因此一定时间内执行一段代码,为了控制要执行的代码,就有一个javascript任务队列。
调用setTimeout()之后,该方法会返回一个数值ID,表示超时调用,这个超时调用ID是执行代码的唯一标识符,可以通过它来取消超时调用。
//设置超时调用
var timeoutId = setTimeout(function() {
alert("hello world");
}, 1000)
//取消
clearTimeout(timeoutId );
//这个弹出框并不会弹出
间歇调用会按照指定的时间间隔重复执行代码,直到间歇调用被取消或者页面被卸载。其他同超时调用类似的。
间歇调用的问题,后一个间歇调用可能会在前一个间歇调用结束之前启动。
//在全局作用域下,调用 setTimeout
function fn(value){
alert("value=" + value);
}
setTimeout("fn(1)", 1000);
//在一个闭包里面调用
function outerFn(){
var value = 1;
function fn(){
alert("value=" + value);
value += 1;
}
setInterval("fn()", 3000);
}
outerFn(); //会出现错误:Uncaught ReferenceError: fn is not defined
//原因是fn()是以字符串的方式传递的,它的作用域是全局作用域,全局作用域是无法访问到fn()的。
//对于上面的这个问题,可以函数的引用传递
function outerFn(){
var value = 1;
function fn(){
alert("value=" + value);
value += 1;
}
setInterval(fn, 3000);
}
outerFn();
//但是这样又带来问题,如果想给fn传参数怎么办?可以像如下这样去写吗?
function outerFn(){
var value = 1;
function fn(n){
alert("value=" + n);
}
setTimeout(fn(5), 1000);
}
outerFn();
//答案是不可以的,函数只写函数名,是函数引用;后面加括号是函数执行
// setTimeout(fn, 1000); //fn的引用
// setTimeout(fn(5), 1000); //fn直接执行
//没有按照预期延迟1000毫秒执行fn(5),而是立刻就执行了。
// 这要注意和传递代码字符串的不同。
//如果确实有从外部传参的需要,该怎么办呢?
function outerFn(value){
function fn(){
alert("value=" + value);
}
setTimeout(fn, 1000);
}
outerFn(5);
是利用了闭包的原理,fn作为内部函数,是可以访问包含它的outerFn的作用域中的变量的,因此我们想给fn传参,只要给outerFn传参就可以了。这在传递的参数复杂(比如是一个复杂的json)的情况下,很有用途。
function createPerson(name, age, job){
var o= new Object();
//创建一个对象,
o.name = name;
//给对象添加相应的属性
o.age = age;
o.job = job;
//给对象添加方法
o.sayName = function(){
//this,代表这个作用域中的的name属性
alert(this.name);
}
//然后把这个对象,返回
return o;
}
var person1 = createPerson("nicholas", 29, "software engineer");
var person2 = createPerson("gery", 27, "doctor");
console.log(person1 instanceof Object);
//返回的true;
console.log(person2 instanceof Object);
//返回时true
console.log(person1 instanceof o);
//这样会报错,提示o未定义,因为o不在此定义域中,访问不到。所以并没有
//解决对象识别的问题
工厂模式解决了创建多个相识对象的问题,但却没有解决对象识别的问题。
在ECMAScript中的构造函数可以用来创建特定类型的对象,例如Object和Array这样的原生构造函数,在运行中出现在特定的执行环境。
自定义创建构造函数,从而定义自定义对象类型的属性和方法。例如
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
}
}
var person1 = new Person("nicholas", 29, "software engineer");
var person2 = new Person("gery", 27, "doctor");
console.log(person1 instanceof Object); //true
console.log(person1 instanceof Person) //true;
console.log(person2 instanceof Object); //true
console.log(person2 instanceof Person) //true;
person1和person2分别保存着Person的一个不同的实例,这两个对象都有一个constructor(构造函数的属性),该属性指向Person,person1和person2之所以同时是Object的实例,是因为所有的对象均继承自Object。
//前面的定义的Person函数的两种使用方法
//第一种,当做构造函数来使用
var person = new Person("nicholas", 29, "software engineer");
person.name(); //nicholas;
//第二种,当做一般函数来使用
Person("jason", 29, "software engineer"); //添加到window
window.sayName(); //jason
//又例如,在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "krsy",35,"Nurse");
o.sayName() //krsy;
console.log(person1.sayName == person2.sayName); //false
然而,对于完成相同任务而创建两个不同的Function实例,是完全没有必要的。而且有this对象的存在,根本不用在执行代码之前就把函数绑定到特定的对象。可以像下面这样的
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
但是这样做也有两个缺陷
我们创建的每一个函数都一个原型(prototype)的属性,这个属性是一个指针,指向一个对象,而使用这个对象的用途是包含可以由特定类型的所有实例共享的实例和方法。
无论任何时候,只要创建一个新函数,就会根据一组特定的规则为该函数创建一个prototype的属性,该属性指向函数原型对象,在默认的情况,所有原型对象都会获得一个constructor(构造函数)的属性。这个属性是包含一个指向prototype属性的所在函数的指针。
//定义一个构造函数
function Person() {};
//给改函数的原型添加属性和方法
Person.prototype.name = "jerry";
Person.prototype.age= 20;
Person.prototype.job = "teacher";
Person.prototype.sayName = function(){
alert(this.name);
}
//构造函数的实例
var person1 = new Person();
person1.sayName();
//jerry
var person2 = new Person();
person2.sayName();
//jerry
构造函数Person有个prototype的属性,这是一个指向函数原型对象的指针,再对改函数原型添加属性和方法,而person1和person2分别是构造函数Person的实例。那么就代表也有prototype属性也是指向Person构造函数的原型函数。也是一个指针。所以person1与person2与Person并没有直接的关系。而连接是实例与原型对象之间虽然person1与person2没有属性和方法,我么却可以通过查找对象属性的过程来实现。
在所有的实现中都无法访问到[[Prototype]],但是isPrototypeOf方法可以用来确定对象之间是否存在着这种关系。
//还是上面的那个例子
console.log(Person.prototype.isPrototypeOf(person1));
//返回true
console.log(Person.prototype.isPrototypeOf(person2));
//返回true
在ECMAScript中的Object.getPrototypeOf()的新增的方法,这个方法返回的是[[prototype]]的值。
//还是上面的那个例子
console.log(Object.getPrototypeOf(person1) == Person.prototype);
//返回true
console.log(Object.getPrototypeOf(person1).name);
//返回jerry
虽然可以通过对象实例访问保存在原型中的值,但对象实例却不能重写对象原型中的值。
例如我们在对象实例添加了一个属性,而改属性与对象原型是一个同名的属性,那么根据上面的对象属性的搜索原理,对象实例中的属性会屏蔽对象原型中的同名属性。
//还是上面的那个例子
var person1 = new Person();
var person2 = new Person();
person1.name = "tank";
console.log(person1.name);
//tank,来自实例
console.log(person2.name);
//jerry来自原型
这样就是说,在实例对象添加的一个属性,会阻止我们访问对象原型的那个同名属性,但是并不会修改在原型对象的那个同名属性的那个值,就是那个值一直都在,只是被屏蔽了。
delete操作符可以完全删除实例的属性,从而让我们重新访问原型中的那个属性
//还是上面的那个例子
var person1 = new Person();
var person2 = new Person();
person1.name = "tank";
console.log(person1.name);
//tank,来自实例
delete person1.name;
console.log(person1.name);
//jerry来自原型
hasOwnProperty()方法可以检查一个属性是存在原型还是实例中,这个方法(记住是继承与Object)字在给定属性存在对象实例中才返回true
//还是上面的那个例子
var person1 = new Person();
var person2 = new Person();
person1.name = "tank";
console.log(person1.hasOwnProperty("name"));
//tank来自实例,所以存在返回true
delete person1.name;
console.log(person1.hasOwnProperty("name"));
//来自原型,所以存在返回false
第一种:单独使用in操作符
in操作符会在通过对象能过访问给定属性时,返回true,无论该属性是存在实例,还是原型中。
//还是上面的那个例子
var person1 = new Person();
var person2 = new Person();
person1.name = "tank";
console.log(person1.hasOwnProperty("name"));
//tank来自实例,所以存在返回true
console.log("name" in person1);
//能通过对象找到,那么就返回true
delete person1.name;
console.log(person1.hasOwnProperty("name"));
//来自原型,所以存在返回false
console.log("name" in person1);
//返回true,能通过对象找到,就返回true
第二种:在for-in循环中使用
在使用for-in循环的时候,返回是所有能够通过对象访问的,可枚举的属性,其中包括存在与实例中的属性,也包括存在存在原型的属性。-----屏蔽了原型中不可枚举属性的实例属性也会在for-in循环中返回(根据规定,所有开发人员定义的属性都是可枚举的--只有在IE8及更早的浏览器除外)
在早期的IE浏览器存在一个bug,那就是屏蔽了原型中不可枚举属性的实例属性不会出现在for-in循环中,
例如:
var o = {
//重新原型中toString方法
toString : function() {
return "my object";
}
}
for(var prop in o) {
if(prop == "toString") {
console.log("found toString"); //在早期ie的浏览器不会显示
}
}
因为在早期的IE中,认为原型中的toString的方法是不可枚举的,所以跳过该属性了。这样我们就看不到控制台的输出。该bug会影响默认不可枚举的所有属性。包括以下:
这个方法是取得对象上所有可枚举的实例属性。该方法接受一个对象作为作为一个参数,返回一个包含所有可枚举属性的字符串的数组。
//还是那个Person()构造函数
var keys = Object.keys(Person.prototype);
console.log(keys);
//返回name, age, job, sayName
var p = new Person()
p.name = "hello";
p.age = 20;
var pkeys = Object.keys(p);
console.log(pkeys);
//name, age
该方法会得到所有的实例属性,无论其是否可枚举。
//是上面的那个例子
var key = Object.getOwnPropertyNames(Person.prototype);
console.log(key)
//constructor,name, age, job, sayName
其中包含了不可枚举的constructor属性。
在上面对定义,都会敲一遍Person.prototype。为了减少不必要的输入,利用对象字面量方式来创建原型对象。
function Person(){};
//对象字面量的方式来创建
Person.prototype = {
name : "jerry",
age : 20,
job : "teacher",
sayName : function(){
alert(this.name);
}
};
用对象字面量的形式创建原型对象与上面的方式有一样的结果,但是有一个问题,那就是constructor属性不在指向Person。具体分析如下:
//上一个例子
var friend = new Person();
console.log(friend instanceof Object ); //true
console.log(friend instanceof Person ); //true
console.log(friend.constructor == Person ); //false
console.log(friend.constructor == Object ); //true
在用instanceo操作符测试Object和Person仍然返回true,但是constructor属性则等于Object,而不等于Person。如果这个constructor属性很重要的话,我们可以向下面这样设置
function Person(){};
//对象字面量的方式来创建
Person.prototype = {
constructor : Person,
name : "jerry",
age : 20,
job : "teacher",
sayName : function(){
alert(this.name);
}
};
这样使用对象字面量创建的时候,包含了一个constructor属性,并将其设置为Person,从而确保通过该属性能够访问适当的值。注意,以这种方式重设constructor属性,会导致他的[[Enumerable]]的特性为true,因为开发人员的自定义的属性都是可枚举的。而原生的constructor属性是不可枚举。
由于在原型中查找值的过程是一次搜索的过程,因此我们对原型对象所做的任何修改都能改立即从实例上反映出来---即使先创建了实例后,修改原型也照旧如此。
//还是上面的的那个例子
var friend = new Person();
Person.prototype.sayHi = function(){
console.log("hi");
};
friend.sayHi(); //弹出了hi
尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来。但是如果重写整个原型对象,那么情况就不一样了。请看下面的例子
//创建构造函数
function Person(){};
//构造函数的实例化
var friend = new Person()
//对象字面量的方式来创建
Person.prototype = {
constructor : Person,
name : "jerry",
age : 20,
job : "teacher",
sayName : function(){
alert(this.name);
}
};
friend.sayName();
//会报错
我们对上面的这个过程进行分析一下:
所以在实例化friend的原型中并没该方法,所以才会报错。
所有原生的引用类型,都是采用这种模式创建的。所有的原生的引用类型(Object, Array, String等等)都在其构造函数的原型上面定义了方法。例如,在Array.prototype中可以找到sort()方法,而在String。prototype上面可以找到subString()。
console.log(typeof Array.prototype.sort); //function
console.log(typeof String.prototype.subString) //function;
通过原生的原型对象,不仅可以取得所用默认方法的引用,而且可以定义新的方法,可以像修改自定义对象的原型一样修改原生的原型对象。因此可以随时添加方法。
//给String类型的原型添加方法
String.prototype.startsWith = function(text) {
return this.indexOf(text) == 0;
}
//调用原型的方法
var msg = "hello world";
console.log(msg.startsWith("hello"));
//在开头找到hello,所以返回true
对于重写原生的原型对象。尽管我们可以这样做,但是不推荐这样做。因为如果只是因为某个实现缺少某个方法,就在原型对象添加方法,那么当另一个支持该方法的实现中运行代码,可能出现命名冲突。
也可能意外的重写原生方法。
//创建构造函数
function Person(){};
//对象字面量的方式来创建
Person.prototype = {
constructor : Person,
name : "jerry",
age : 20,
job : "teacher",
friend : ["xiaoming", "xiaohong"],
sayName : function(){
alert(this.name);
}
};
//**构造函数的实例化(注意这个写在原型的前面和后面的区别)**
var person1 = new Person();
var person2 = new Person();
//本意是对person1单独添加
person1.friend.push("xiaohua");
console.log(person1.friend);
//返回"xiaoming , xiaohong,xiaohua"
console.log(person2.friend);
//返回"xiaoming , xiaohong,xiaohua"
console.log(person1.friend == person2.friend)
//返回是true
本来想对实例person1单独添加添加,却因为该属性是在原型中,所有的实例同时共享的,改了person1的实例,其实是在修改原型中的属性,而原型中的属性是所有实例共享的,所以,导致所有实例都拥有了这个本来想单独添加给实例person1的,所以,原型最大优势是所有实例的共享其方法和属性,但同时也是最大的劣势。所以很少单独使用原型模式。一般都是与其他模式结合着一起使用。
//定义的构造函数
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friend = ["xiaoming", "xiaohong"];
}
//重写构造函数的原型
Person.prototype = {
constructor : Person,
sayName : function() {
console.log(this.name);
}
}
//构造函数的实例化
var person1 = new Person("xiaohua", 20, "teacher");
var person2 = new Person("xiaoli", 30, "dancer");
//对person1的friend属性数组添加一个
person1.friend.push("xiaofeng");
//输出person1和person2的friend属性进行比较
console.log(person1.friend);
//"xiaoming, xiaohong,xiaohua “
console.log(person2.friend);
//"xiaoming, xiaohong,"
console.log(person1.friend == person2.friend);
//输出的是false
//比较person1和person2的sayName方法是不是来自原型
console.log(person1.sayName == person2.sayName);
//true
在这个例子中,实例属性都是在构造函数中定义的,而所有实例共享的属性constructor和方法而是在原型中定义的。所以修改了person1.friend并不会影响person2.friend。因为他们分别引用了不同的数组。
构造函数和原型的组合使用是目前在ECMAScript中使用最广泛,认同度最高的一种自定义类型的方法,
可以说是用来定义引用类型的默认模式。
动态原型模式是把所有信息都封装在构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持同时使用构造函数与原型的优点。那就是说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化。
function Person(name, age, job) {
//属性
this.name = name;
this.age = age;
this.job = job;
//方法
if(typeof this.name != function){
Person.prototype.sayName = function() {
console.log(this.name);
};
}
}
var friend = new Person("xiaoding", 29, "singer");
friend.sayName();
这里的if判断语句,判断只要在sayName()的方法不存在的时候,才为原型添加该方法。这段代码只会在初次调用构造才会执行。此后原型以及完成了初始化。不需要再做什么修改。还有一点记住,这里对原型所做的修改,会立即在所有实例中得到反映。这种方法是相当的不错的。
因为我们并没有使用对象字面量来重写原型,所以构造函数与原型的联系还在,所有的实例与原型的联系也还在。
通常在前几种模式都不适用的话,就可以使用寄生构造函数模式。
这个模式的基本**是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后返回新创建的对象
但是从表面上看,这个函数又像典型的构造函数。
function Person(name, age, job) {
//对象的一个实例
var o = new Object()
//给该实例添加属性;
o.name = name;
o.job = job;
o.age = age;
o.sayName = function(){
console.log(this.name);
};
//返回该对象
return o;
}
var friend = new Person("jerry", 30, "doctor");
friend.sayName();
这个模式其实跟工厂模式是一模一样的。构造函数在不返回值的情况下,默认会返回对象的而新实例。而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。
这个模式在特殊情况下,为对象创建构造函数,例如,我们想创建一个具有额外方法的特殊数组。由于不能修改Array()构造函数,因此可以使用这个模式。
function SpecialArray(){
//创建数组
var values = new Array();
//添加值
values.push.apply(values, arguments);
//添加方法
values.toPipedString = function(){
return this.join(“|”);
}
//返回数组
return values;
}
var colors = new SpecialArray("red", "blue");
console.log(colors.toPipedString)
//返回"red | blue"
对于寄生构造函数模式,要注意的有两点:
由于存在以上问题,当我们么能有其他模式解决的话,都不要用这种模式
稳妥模式就是说:指的是没有公共属性,而且其方法也不能用this的对象。
稳妥模式适用于:
稳妥模式与寄生构造函数类似的模式,但有两点区别:
function Person(name, age, job) {
//创建要返回的对象
var o = new Obiect();
//可以在这里添加私有变量和函数
//添加方法
o.sayName = function(){
console.log(name);
}
//当成一般函数来使用
var friend = Person("jerry", 29, "doctor");
friend.sayName();
//返回jerry
}
对稳妥模式说明一下,以这种模式创建的对象中,除了使用sayName()方法之外,没有其他方法访问name的值。变量Person中保存一个稳妥对象,除了调用sayName方法外,没有别的方法访问其他数据成员。即使有其他代码会给这个对象添加方法和数据成员,但也不可嫩有别的方法来访问传入到构造函数中的原始值。
注意与寄生构造函数模式一样,此时用构造函数创建的对象与构造函数之间并没有关联,用instanceof操作符对创建的对象没有意义。
<!-- 点击按钮,弹出遮罩及弹出框 -->
<span class="add-plan"></span>
<!-- 弹出的遮罩层 -->
<div class="shade-layer"></div>
<!-- 弹出框 -->
<div class="pop-box">
<h3>是否加入行程</h3>
<span>取消</span>
<span>确认</span>
</div>
.shade-layer {
position: fixed;
top: 0;
left: 0;
z-index: 2;
display: none;
width: 100%;
height: 100%;
background: #000;
opacity: 0.6;
}
.pop-box {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 3; /*这个是为了弹出层的层级高于body里面的内容,显示出来*/
margin: auto; /*让一个div垂直水平居中*/
display: none;
width: 10rem;
height: 5rem;
font-size: 0; /*去掉内联元字符带的间距*/
background: #fff;
}
.pop-box span {
display: inline-block;
margin-left: 0.5rem;
padding-left: 1.4rem;
padding-right: 1.4rem;
line-height: 1.5rem;
font-size: 0.75rem;
color: #fff;
background-color: #999;
border-radius: 2px;
text-align: center;
}
.pop-box span:last-child {
background-color: #fdbc31;
}
.delete-pop-box span:last-child {
background-color: #51b308;
}
//引入了jquery
$(document).ready(function () {
$(".add-plan").click(function(event) {
$(".shade-layer").show();
$(".pop-box").show();
$("body").css('overflow', 'hidden');
//这个是为了弹出遮罩的时候,body里面的内容不能移动。
});
$(".pop-box span").click(function(event) {
$(".shade-layer").hide();
$(".pop-box").hide();
$("body").css('overflow', 'visible');
//弹出层消失点后,body里面的内容可以进行移动。
});
});
<ul class="selected-month clearfix" id="selected-month">
<li class="selected-small-title selected-current">11月份·精选</li>
<li class="selected-small-title">12月份·精选</li>
<li class="selected-small-title">1月份·精选</li>
</ul>
.selected-month {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
border-top: 1px solid #e1e1e1;
border-bottom: 1px solid #e1e1e1;
background-color: #fff;
}
.selected-month-fixed {
position: fixed;
left: 0;
top: 0;
}
.selected-small-title {
float: left;
margin-left:-1px;
width: 5.27rem;
font-size: 0.75rem;
color: #888;
line-height: 2rem;
text-align: center;
border-right: 1px solid #e1e1e1;
border-left: 1px solid #e1e1e1;
}
.selected-current{
color: #ffa030;
}
//引入了jquery
$(document).ready(function () {
//滚动到一定高度,导航固定
$(window).scroll(function(){
//window对象即窗口开始滚动时
var scrolltop = $(this).scrollTop();
//当window对象滚动到一定的距离时,tab菜单固定在顶部,通过添加class
//当window对象滚动到小于一定的距离时,移除class,这tab菜单,不固定在顶部
if( scrolltop >= 370){
$("#selected-month").addClass("selected-month-fixed");
}else{
$("#selected-month").removeClass("selected-month-fixed");
}
});
<nav>
<ul class="view-details-qhnav" id="switchnav">
<li class="qhnav-current">旅游攻略</li>
<li>游记</li>
<li>旅游趣事</li>
</ul>
</nav>
<div class="switchnav-wrapper">
<div class="switchnav-conntent">我是旅游攻略的内容</div>
<div class="switchnav-conntent">我是游记内容</div>
<div class="switchnav-conntent">我是旅游趣事的内容</div>
</div>
/*这个是移动端的布局*/
.view-details-qhnav {
display: flex;
display: -webkit-flex;
/*for uc weixin */
display: -webkit-box;
width: 100%;
padding-top:0.25rem;
padding-bottom: 0.25rem;
-webkit-box-pack: justify;
-moz-box-pack: justify;
box-pack: justify;
-webkit-box-align: center;
-moz-box-align: center;
box-align: center;
border-bottom: 1px solid #dadada;
background-color: #fff;
}
.qhnav-current {
color: # #51b308;
}
/*这个选中tab的下划线的效果*/
.qhnav-current:after {
content: "";
position: absolute;
left:0.25rem;
bottom: -0.25rem;
height: 2px;
width: 90%;
background-color: #51b308;
}
.switchnav-conntent {
display: none;
}
.switchnav-wrapper .switchnav-conntent:first-child {
display:block;
}
//引入了jquery
$(document).ready(function () {
$('#switchnav li').click(function(event) {
//获取点击的当前的li的索引值
var indexnav = $(this).index();
//让当前点击的li标签处于选中状态,其兄弟元素移除选中状态
$(this).addClass('qhnav-current').siblings('li').removeClass('qhnav-current');
//让内容区域的中相等的索引值的区域显示,其他的兄弟元素隐藏
$('.switchnav-wrapper .switchnav-conntent').eq(indexnav)
.show().siblings('.switchnav-conntent').hide();
});
});
<div class="view-details-ticket">
<!-- 这个是下拉箭头的元素 -->
<span class="ticket-arrow-up"></span>
<h3 class="view-ticket-title">单门票</h3>
<div class="view-ticket-c">
<p class="view-ticket-list">峨眉山成人票</p>
<p class="view-ticket-price">¥68<span class="ticket-explain">票型说明</span></p>
</div>
</div>
.ticket-arrow-up, .ticket-arrow-down {
float: right;
width: 1.1rem;
margin-top: 0.6rem;
margin-right: 0.5rem;
height: 15px;
/*这个是箭头上下合在一张的图片*/
background-image: url(../images/ticket-more.png);
background-size: 15px ;
background-repeat: no-repeat;
background-position: left top;
}
.ticket-arrow-down {
background-position: left bottom;
}
//引入了jquery
$(document).ready(function () {
//景区详情中门票的切换效果以及弹窗(第一种方法,背景图的时候,添加togglClass())
$('.ticket-arrow-up').click(function() {
//toggleClass() 方法对添加和移除被选元素的类进行切换。
//这个class有的背景图进行了定位,变成了收起的箭头
$(this).toggleClass('ticket-arrow-down');
//这个兄弟元素view-ticket-c在显示与隐藏进行切换。
$(this).siblings('.view-ticket-c').toggle();
});
//(第二种方法,前景图判断src,来通过src来改变)
//这个html结构是的下拉收起的图标是前景图
$(".insurance-details").click(function(event) {
//点击出现的内容,显示或隐藏出现的内容
$(this).siblings(".insurance-explain").toggle();
//下拉收起的图标是前景图,通过src来判断,进行切换
if ($(this).children().attr("src") =="images/drop_down_icon.png") {
$(this).children().attr("src", "images/drop_up_icon.png");
} else {
$(this).children().attr("src", "images/drop_down_icon.png");
}
});
//(第三种方法,背景图下拉收起的图标放在一张图中,通过css定位来判断)
$(".on-off").click(function(event) {
//点击出现的内容,显示或隐藏出现的内容
$("#insur-content").toggle();
//下拉收起的图标是背景图,背景图的定位来判断
//背景图如果定位是左边,顶部
if ($(this).css("background-position")=="0% 0%") {
//那么把背景图改为定位是右边,顶部
$(this).css("background-position","right top");
} else {
$(this).css("background-position","left top");
}
});
});
//(第四种方法,通过添加状态)
<div id="back-to-top" class="back-to-top">
<!-- 这个是点击回到顶部的图片-->
<img src="images/back_to_top.png"/>
</div>
.back-to-top {
position:fixed;
display:none;
width:32px;
height:32px;
bottom:10px;
right:10px;
}
.back-to-top img {
display: inline-block;
width: 100%;
height: 100%;
}
//引入了jquery
$(document).ready(function () {
//当window对象开始滚动时
$(window).scroll(function(){
//当window对象滚动离顶部的距离大于80px时
if ($(window).scrollTop()>80){
//back-to-top的元素渐入出现
$("#back-to-top").fadeIn(1500);
}
//当window对象滚动离顶部的距离小于等于80px时
else {
//back-to-top的元素逐渐消失
$("#back-to-top").fadeOut(1500);
}
});
//当点击跳转链接后,回到页面顶部位置
$("#back-to-top").click(function(){
//回到页面顶部位置,有一个逐渐过渡回到顶部的效果
$('body,html').animate({scrollTop:0},800);
});
});
<!-- 用户协议 -->
<label for="" class="user-protocol">
<!-- 隐藏浏览器自带样式 -->
<input type="checkbox" id="agree" class="hidden-input" name="agree">
<!-- 自定义复选框样式 -->
<span></span>
同意
<a href="#" class="protocol-link">《盲游用户注册协议》</a>
</label>
<!-- 注册完成按钮 -->
<button class="login-button">完成</button>
.hidden-input {
position: absolute;
z-index: 10; /*定位高了才能让input被看见*/
width: 1rem;
height: 3rem;
opacity: 0;
/*opacity是为了让input按钮隐藏起来*/
}
input[type="checkbox"]+span {
/* your style goes here */
display: inline-block;
height: 0.75rem;
width: 0.75rem;
border-radius: 4px;
border: 2px solid #51b308;
margin-right: 0.25rem;
vertical-align: middle;
}
/* active style goes here */
input[type="checkbox"]:checked+span {
background-image: url(../images/correct.png);
background-repeat: no-repeat;
background-size: 12px 9px;
background-position: center;
}
//引入了jquery
$(document).ready(function () {
$("#agree").click(function(event) {
//点击复选框时,如果是被选中的状态那么注册按钮,处于选中状态
if($("#agree").prop("checked")){
$(".login-button").css('background-color', '#51b308');
$(".login-button").click(function(event) {window.location.href="mine.html";});
} else {
//点击复选框时,如果是没有选中,那么变成未选中状态
$(".login-button").css('background-color', '#999');
}
});
});
<!-- gery为灰色的星星,另一个是选中的星星 -->
<span class="pln-com-sta">服务评分:
<img src="images/comment_gery_icon.png">
<img src="images/comment_gery_icon.png">
<img src="images/comment_gery_icon.png">
<img src="images/comment_gery_icon.png">
<img src="images/comment_gery_icon.png">
</span>
//引入了jquery
$(document).ready(function () {
$(".pln-com-sta img").click(function(event) {
//点击当前的星星的时候,如果是选中的时候,
if ($(this).attr("src")=="images/comment_icon.png") {
//把其src设置未选中的状态
$(this).attr("src", 'images/comment_gery_icon.png');
//把当前点击的元素,前面的元素全都变为选中的样式
$(this).prevAll().attr("src", 'images/comment_icon.png');
//把当前点击的元素,的后面元素全都变为灰色
$(this).nextAll().attr("src", 'images/comment_gery_icon.png');
} else {
//点击当前的星星的时候,如果是未选中的样式,把其src设置选中的状态
$(this).attr("src", 'images/comment_icon.png');
//把当前点击的元素,前面的元素全都变为选中的样式
$(this).prevAll().attr("src", 'images/comment_icon.png');
//把当前点击的元素,前面的元素全都变为未选中的样式
$(this).nextAll().attr("src", 'images/comment_gery_icon.png');
}
});
});
<div class="consult-planner-day">服务天数
<!-- 添加天数的按钮 -->
<div class="addday-button">
<span class="decrease-number">一</span>
<span class="increase-number">+</span>
<input type="number" class="addday" value="0">
</div>
</div>
//引入了jquery
$(document).ready(function () {
//点击加号时
$(".increase-number").click(function(){
//取出input现在的值,并使用parseInt转为int类型数据
var oldValue=parseInt($(this).siblings(".addday").val());
//oldValue变量自加1
oldValue++;
//将增加后的值付给原控件(即input元素的值)
$(this).siblings(".people-number").val(oldValue);
});
//点击减号的时候
$(".sdecrease-number").click(function(){
//取出现在的值,并使用parseInt转为int类型数据
var oldValue=parseInt($(this).siblings(".speople-number").val());
//为了防止出现人数的负数,做出一些判断
if (oldValue <= 0) {
oldValue = 1;
} else {
//自加1
oldValue--;
//将增加后的值付给原控件
$(this).siblings(".speople-number").val(oldValue);
});
<div class="planner-js">
<h3>简介</h3>
<p id="spread-content" class="spread-content">我是内容,我是内容</p>
</div>
<span class="palnner-js-more">展开</span>
.planner-js {
font-size: 0.65rem;
color: #303030;
}
.planner-js h3 {
padding-top: 0.3rem;
line-height: 1.1rem;
}
.planner-js p {
color: #888;
line-height: 1.1rem;
}
.palnner-js-more{
display: block;
padding-right: 1rem;
font-size: 0.55rem;
color: #888;
line-height: 1.5rem;
text-align: right;
background-image: url(../images/planner_more.png);
background-size: 10px 6px;
background-repeat: no-repeat;
background-position: right center;
}
//引入了jquery
$(document).ready(function () {
//这个是第一次获取内容里面全部的字符串。赋值给变量
var value=$("#spread-content").text();
//对内容里面的字符串进行截取,按照一定的个数进项截取,添加省略号
$("#spread-content").text(value.substring(0,44)+"...");
//点击展开元素
$(".palnner-js-more").click(function(event) {
//获取只有一部分内容的div的高度
var currentHeight = $("#spread-content").height();
//当点击元素时,判断高度,如果高度小于80px时,那么此时内容是被收缩起来的。
if (currentHeight<80) {
//因为此时内容是被收缩,点击的时候,就是要展开内容。所以要把全部字符串显示完全。
$("#spread-content").text(value);
//对此,把内容区域的高度相应的增高一些。
$("#spread-content").height("4rem");
//但是,并不是为内容有多少,内容区域的高度就有多高,所以出现滚动条
$("#spread-content").css("overflow", "auto");
//点击后,内容显示完全,下一步就是收起,所以设置为收起
$(this).text("收起");
//相应的图标的方向也发生了改变
$(this).css("background-image", "url(images/planner_less.png)");
} else {
//因为此时内容是全部显示的,所以下面的是收起的内容的样式。
$("#spread-content").text(value.substring(0,44)+"...");
$("#spread-content").height("2rem");
$("#spread-content").css("overflow", "hidden");
$(this).text("展开");
$(this).css("background-image", "url(images/planner_more.png)");
}
});
});
一些感悟:在写这个效果的时候,由于才开始自学前端,大部分都是上面搜索相关教程,可能很多写法都不够准确,或者说是不够简洁,不够完善,在以后会慢慢对这方面加强改善,但是这种一点点的进步的感觉真的好,前端可能入门较其他的相对来说容易一些,但是要成为一个好前端,那么是需要很大耐心的,以及一颗不断的学习的心。多看书,多看源码,多实践,多向前辈学习。
<li class="common-passenger-item credential">
证件类型
<span class="card-type">身份证</span>
<span class="card-pull-down"></span>
<ul class="pop-idcard">
<li>暂无证件</li>
<li>身份证</li>
<li>护照</li>
<li>军官证</li>
<li>港澳通行证</li>
<li>**通行证</li>
<li>台胞证</li>
<li>回乡证</li>
<li>户口薄</li>
<li>出生证明</li>
<li>其他</li>
</ul>
</li>
.credential {
position: relative;
}
.pop-idcard {
display: none;
position: absolute;
right: 0;
top:1.75rem;
width: 7.75rem;
background-color: #eee;
box-shadow: 0 0 16px rgba(0,0,0,0.3);
}
.pop-idcard li {
width: 7.75rem;
padding-left: 0.25rem;
font-size: 0.65rem;
color: #303030;
line-height: 1.2rem;
border-bottom: 1px solid #dadada;
}
//添加乘客信息证件类型的效果
//点击绿色小三角
$(".card-pull-down").click(function(event) {
//证件类型的弹窗显示或隐藏的切换
$(".pop-idcard").toggle();
});
//点击证件类型的具体项
$(".pop-idcard li").click(function(event) {
//获取点击当前的字符串值
var cardClass = $(this).text();
//把证件类型里面显示的字符串换成弹出选项的那个证件类型
$(".card-type").text(cardClass);
//把证件类型弹出框隐藏
$(".pop-idcard").hide();
});
<!-- 下面是结构,css就不贴出来了 -->
<ul class="booking-city-select clearfix">
<li class="booking-setoff">
<span>出发城市</span>
<input type="text" value="成都" id="single-strat-city">
</li>
<li class="exchange-button" id="single-exchange"></li>
<li class="booking-return">
<span>抵达城市</span>
<input type="text" id="single-end-city" placeholder="请选择城市" value="西藏">
</li>
</ul>
//点击单程交换按钮
$("#single-exchange").click(function(event) {
//获取单程开始的城市的字符串
var singleStartCity = $("#single-strat-city").val();
//获取单程目的城市字符串
var singleEndCity = $("#single-end-city").val();
//把单程开始的城市字符串设置为单程结束城市
$("#single-strat-city").val(singleEndCity);
//把单程结束的城市字符串设置为单程开始城市
$("#single-end-city").val(singleStartCity);
});
<!-- 固定footer区域 开始-->
<footer class="ticket-bottom-bar">
<div id="sort">
<img src="images/filter_icon.png"><span>筛选</span>
</div>
<div id="time-sort">
<img src="images/time_icon.png"><span>时间</span>
</div>
<div id="price-sort">
<img src="images/price_icon.png"><span>价格</span>
</div>
</footer>
<!-- 固定footer区域 结束-->
//时间的筛选,当点击时间图标的元素时,
$("#time-sort").click(function(event) {
//价格图标由绿色变成白色未选中的图标
$("#price-sort img").attr("src", "images/price_icon.png");
//价格里面的span的字颜色变成未选中的样式,白色
$("#price-sort span").css("color", "#fff");
//价格里面的span的字变成未选中的“价格”二字
$("#price-sort span").text("价格");
//判断时间里面的图标处于那种状态
//判断时间里面的图标处于那种状态,如果未选中的状态时,
if ($("#time-sort img").attr("src")=="images/time_icon.png") {
//时间处于未选中时,那么点击的时候,其图标变成向上的绿色箭头
$("#time-sort img").attr("src", "images/reorder_up.png");
//字的颜色变成绿色
$("#time-sort span").css('color', '#51b308');
//字的改变成为“时间从早到晚”
$("#time-sort span").text("时间从早到晚")
}
//判断时间里面的图标处于那种状态,如果处于向上的绿色箭头时,
else if($("#time-sort img").attr("src")=="images/reorder_up.png") {
//如果处于向上的绿色箭头时,那么点击变成向下的绿色箭头,下面与上面的原理类似
$("#time-sort img").attr("src", "images/reorder_down.png");
$("#time-sort span").css("color", "#51b308");
$("#time-sort span").text("时间从晚到早");
}
else{
$("#time-sort img").attr("src", "images/reorder_up.png");
$("#time-sort span").text("时间从早到晚");
}
});
//价格的筛选(下面与上面的原理时间的筛选原理类似)
$("#price-sort").click(function(event) {
$("#time-sort img").attr("src", "images/time_icon.png");
$("#time-sort span").css("color", "#fff");
$("#time-sort span").text("时间");
if ($("#price-sort img").attr("src")=="images/price_icon.png") {
$("#price-sort img").attr("src", "images/reorder_up.png");
$("#price-sort span").css("color", "#51b308");
$("#price-sort span").text("价格从低到高");
}
else if($("#price-sort img").attr("src")=="images/reorder_up.png") {
$("#price-sort img").attr("src", "images/reorder_down.png");
$("#price-sort span").css("color", "#51b308");
$("#price-sort span").text("价格从高到低");
}
else{
$("#price-sort img").attr("src", "images/reorder_up.png");
$("#price-sort span").text("价格从低到高");
}
});
//起飞时间段筛选(只贴出那个的钩怎样选中的,其他原理跟上一篇的效果相同)
//点击筛选按钮,弹出筛选框,点击筛选时间段时
$("#time-slet li").click(function(event) {
//点击当前的时间段时,此时加上一个选中class,其兄弟元素移除掉选中的class
$(this).addClass("timr-sel-current").siblings().removeClass('timr-sel-current');
});
<!-- 这个是要求html5 -->
<div class="upload-avater-box">
<form action="">
<input class="upload-avater" name="imgOne" id="imgOne"
type="file" onchange="preImg(this.id,'imgPre');"/>
<img id="imgPre" class="mine-avater" src="images/default_avater.png" alt="头像">
</form>
</div>
//暂时还看不懂,这段代码,先记录着,等会了,再来;理解
function preImg(sourceId, targetId) {
if (typeof FileReader === 'undefined') {
alert('Your browser does not support FileReader...');
return;
}
var reader = new FileReader();
reader.onload = function(e) {
var img = document.getElementById(targetId);
img.src = this.result;
}
reader.readAsDataURL(document.getElementById(sourceId).files[0]);
}
<ul class="reorder-content">
<li class="reorder-titile">
<span class="reorder-pull-up"></span>
<span class="reorder-pull-down"></span>
<h3>第一天</h3>
</li>
<li class="reorder-titile">
<span class="reorder-pull-up"></span>
<span class="reorder-pull-down"></span>
<h3>第二天</h3>
</li>
<li class="reorder-titile">
<span class="reorder-pull-up"></span>
<span class="reorder-pull-down"></span>
<h3>第三天</h3>
</li>
</ul>
//点击向下的箭头时
$('.reorder-pull-down').click(function(event) {
//先进行判断,当前元素的父元素的下一个元素是否为空,不为空,才能交换
if ($(this).parent().next().text() != "") {
//如果,下一个元素不是为空的话,获取其子元素h3的文本节点
var nextContent = $(this).parent().next().children('h3').text();
//获取当前元素的兄弟元素的h3的文本节点
var currentContent = $(this).siblings('.reorder-titile h3').text();
//实现文本节点进行交换
$(this).parent().next().children('h3').text(currentContent)
$(this).siblings('.reorder-titile h3').text(nextContent) ;
}
});
//点击向上箭头(原理跟上面是类似的,不同的是其判断条件是上一个元素是否为空)
$('.reorder-pull-up').click(function(event) {
if ($(this).parent().prev().text() != "") {
var nextContent = $(this).parent().prev().children('h3').text();
var currentContent = $(this).siblings('.reorder-titile h3').text();
$(this).parent().prev().children('h3').text(currentContent)
$(this).siblings('.reorder-titile h3').text(nextContent) ;
}
});
<!-- 乘客信息区域 开始 -->
<div class="plane-pa-mes">
<h3 class="pa-mes-title">乘客信息<span id="add-passenage">添加乘客</span></h3>
<!-- 乘客信息区域的内容框-->
<div class="pa-mes-details clearfix">
<span class="cancle-passenage"></span>
<ul>
<li>成人票<input type="text" placeholder="姓名"></li>
<li>身份证<input type="text" placeholder="必填"></li>
</ul>
</div>
</div>
<!-- 乘客信息区域 结束 -->
//点击添加乘客按钮
$("#add-passenage").click(function(event) {
//获取添加乘客内容区域的html拼接内容,赋值给变量
var passageContent='<div class="pa-mes-details clearfix"><span class="cancle-passenage"></span><ul><li>成人票<input type="text" placeholder="姓名"></li><li>身份证<input type="text" placeholder="必填"></li></ul></div>';
//第一个内容区域,下面再插入一个内容区域
$(".plane-pa-mes").append(passageContent);
});
//点击取消按钮
$(".cancle-passenage").click(function(event) {
//乘客内容区域消失
$(this).parent(".pa-mes-details").hide();
});
但是,上面有一个问题,就是通过js拼接的乘客内容区域,点击取消按钮时,这个乘客
内容区域却消失不掉。暂时不知道这个原因到底是什么造成的。记录一下,等以后会了,再
解决。
上面的过程就是----- $(".plane-pa-mes").append(passageContent);
找到class为plane-pa-mes的元素,插入拼接拼接内容,再开始执行 $(".cancle-passenage").click(function(event) {$(this).parent(".pa-mes-details").hide();});
找到cancle-passenage这个class,拼接的内容会等渲染进程来开始。但是渲染进程还没开始工作,所以js就找不到这个class。这段js执行不了,等渲染进程开始工作,那么dom就渲染出来了。
<!-- header区域开始 -->
<header class="home-header clearfix">
<a href="index.html" class="home-back-light"></a>
<input class="home-search" type="text" id="des-search" placeholder="请输入
目的地、关键字、城市"/>
<!-- 这个是小叉叉的元素 -->
<span class="des-search-cancle home-search-cancle"></span>
<!-- 这是哪个匹配元素 -->
<div class="home-des-match">国色天香<span>约149个结果</span></div>
</header>
<!-- header区域结束 -->
$('#des-search').focus(function(){
//搜索框获得焦点时,匹配元素的框显示
$(".home-des-match").show();
});
$('#des-search').blur(function(){
//搜索框失去焦点,匹配元素的框消失
$(".home-des-match").hide();
});
$(".des-search-cancle").click(function(event) {
//点击那个小叉叉时,搜索里面内容的消失
$('#des-search').val("");
});
<!-- 添加天数的按钮 -->
<div class="addday-button">
<span class="sdecrease-number">一</span>
<span class="add-day sincrease-number">+</span>
<input type="number" class="speople-number addday" value="1">
</div>
<!-- 列表区域结束 -->
<!-- 左边的导航切换区域 -->
<ul class="destination-nav">
<li class="destination-nav-current">第一天</li>
</ul>
<!-- 右侧区域 -->
<div class="des-nav-wrapper">
<!-- 第一天对应的内容区域 -->
<div class="make-route-content destination-nav-c">
<!-- 标签区域 开始 -->
<div class="match-mark-clist">
<span class="match-mark-citem">峨眉山11</span>
<span class="match-mark-citem">峨眉山峨眉山</span>
<span class="match-mark-citem">峨眉山11</span>
<span class="match-mark-citem">峨眉山11</span>
<span class="match-mark-citem">峨眉山11</span>
</div>
<!-- 标签区域 结束 -->
<!-- 天数对应的区域开始-->
<ul class="make-route-details">
<li>标题<input type="text" placeholder="请填写第一天标题"/></li>
<li>交通<input type="text" placeholder="交通"/></li>
<li>用餐<input type="text" placeholder="用餐"/></li>
<li>住宿<input type="text" placeholder="住宿"/></li>
<li>行程安排</li>
<li class="route-details-plan">
<span class="cancel-yc"></span>
<span class="rount-chan-cont">11</span>
<input type="text" placeholder="请安排行程"/>
</li>
<li class="route-details-plan">
<span class="cancel-yc"></span>
<span class="rount-chan-cont">11</span>
<input type="text" placeholder="请安排行程"/>
</li>
</ul>
<!-- 天数对应的区域结束-->
</div>
<!-- 添加内容区域 开始-->
<div id="addDiv" style="display: none;">
<div class="make-route-content destination-nav-c">
<!-- 标签区域 开始 -->
<div class="match-mark-clist">
<span class="match-mark-citem">峨眉山xx</span>
<span class="match-mark-citem">峨眉山xx</span>
<span class="match-mark-citem">峨眉山xx</span>
<span class="match-mark-citem">峨眉山xx</span>
<span class="match-mark-citem">峨眉山xx</span>
</div>
<!-- 标签区域 结束 -->
<!-- 天数内容区域 开始-->
<ul class="make-route-details">
<li>标题<input type="text" placeholder="请填写第一天标题"/></li>
<li>交通<input type="text" placeholder="交通"/></li>
<li>用餐<input type="text" placeholder="用餐"/></li>
<li>住宿<input type="text" placeholder="住宿"/></li>
<li>行程安排</li>
<li class="route-details-plan">
<span class="cancel-yc"></span>
<span class="rount-chan-cont">11</span>
<input type="text" placeholder="请安排行程"/>
</li>
<li class="route-details-plan">
<span class="cancel-yc"></span>
<span class="rount-chan-cont">11</span>
<input type="text" placeholder="请安排行程"/>
</li>
</ul>
<!-- 天数内容区域 结束-->
</div>
</div>
<!-- 标签对应的内容区域-->
<div id="matchlist" style="display: none;">
<li class="route-details-plan">
<span class="cancel-yc"></span>
<span class="rount-chan-cont">11</span>
<input type="text" placeholder="请安排行程"/>
</li>
</div>
//这个函数是把阿拉伯数字转为中文的大写的数字
function getCapitalNumber(_number){
//先创建一个变量设置为一个数组,为中文的数字
var val=new Array("",
"一","二","三","四","五",
"六","七","八","九","十",
"十一","十二","十三","十四","十五",
"十六","十七","十八","十九","二十",
"二十一","二十二","二十三","二十四","二十五",
"二十六","二十七","二十八","二十九","三十",
"三十一","三十二","三十三","三十四","三十五",
"三十六","三十七","三十八","三十九","四十",
"四十一","四十二","四十三","四十四","四十五",
"四十六","四十七","四十八","四十九","五十",
"五十一","五十二"
);
//把数组里面的元素为中文数字
return val[(_number)];
}
//标签内容的效果,点击标签
$(".match-mark-clist span").click(function(event) {
//取得当前标签的文本内容
var markContent = $(this).text();
//取得匹配标签内容区域的文字改为标签点击的内容
$("#matchlist .rount-chan-cont").text(markContent);
//获得改变改变标签内容的元素的内容
var matchList = $("#matchlist").html();
//插入.make-route-details列表成为子元素
$(".make-route-details").append(matchList);
});
//点击×删除的效果
$(".cancel-yc").click(function(event) {
//点击小叉号,内容区域移除dom文档树,**但是通过动态添加的却没有这个效果**
$(this).parent(".route-details-plan").remove();
});
//点击加号的效果
$(".sincrease-number").click(function(){
//取出现在的值,并使用parseInt转为int类型数据,前面有这个效果介绍
var oldValue=parseInt($(this).siblings(".speople-number").val());
//自加1
oldValue++;
//将增加后的值付给原控件
$(this).siblings(".speople-number").val(oldValue);
//此时调取的是前面的那个函数,转为中文数字
var number=getCapitalNumber(oldValue);
//把获取的天数,拼接字符串成为第几天。
var addText="<li>第"+number+"天</li>"
//在列表的左侧天数区域,在最后一个li元素添加文本节点
$(".destination-nav li:last-child").after(addText);
//同时,在右边的内容区域,添加相应的内容区域
$(".des-nav-wrapper").append($("#addDiv").html());
//点击导航切换事件
$('.destination-nav li').click(function(event) {
var indexnav = $(this).index();
$(this).addClass('destination-nav-current').siblings('li').removeClass('destination-nav-current');
$('.destination-nav-c').eq(indexnav).show().siblings('.destination-nav-c').hide()
//点击右边的内容区中,标签列表实现标签的相对应的内容的添加效果
$('.destination-nav-c').eq(indexnav).find(".match-mark-clist span").click(function(event) {
var markContent = $(this).text();
$("#matchlist .rount-chan-cont").text(markContent);
var matchList = $("#matchlist").html();
$('.destination-nav-c').eq(indexnav).find(".make-route-details").append(matchList);
});
});
});
//点击减号的效果
$(".sdecrease-number").click(function(){
//取出现在的值,并使用parseInt转为int类型数据
var oldValue=parseInt($(this).siblings(".speople-number").val());
if (oldValue>1) {
oldValue--;//自减少1
} else {
oldValue = 1;
}
//点击减号后,如果当前的class为选中状态(这样是为了标明显示状态)
if ($(".destination-nav li").eq(oldValue).attr("class") =="destination-nav-current") {
//那么,当前选中的前一位索引值的右侧内容,显示,
$(".des-nav-wrapper .destination-nav-c").eq(oldValue-1).show();
//那么,当前选中的右侧内容消失
$(".des-nav-wrapper .destination-nav-c").eq(oldValue).remove();
//同时,当前选中的前一位索引值的左侧天数为当前选中
$(".destination-nav li").eq(oldValue-1).addClass('destination-nav-current');
} else {
//如果不是当前选中,那么直接移除掉右侧内容就好了
$(".des-nav-wrapper .destination-nav-c").eq(oldValue).remove();
}
$(this).siblings(".speople-number").val(oldValue);//将增加后的值付给原控件
//左侧天数内容的移除
$(".destination-nav li").eq(oldValue).remove();
});
一些感悟:当我们写js的时候,只要我们把一件事所有的过程,描述清楚了,那么比较容易
写出来。在写js之前,我建议大家,最好用自己的话,分成一条条的,把每一个过程的罗列
出来。先看自己罗列的是不是对了,合理的,有没有考虑没到的地方,当我们把所用的情况
都说清楚了,一条一条理清楚了。那么写js,其实就把我们的思路用code表现出来而已。
在我们公司的官网中项目中导航和footer都是动态加载的
<!-- 公用的导航区域是一个 html页面开始 -->
<!DOCTYPE html>
<html lang="en">
<!-- 头部区域开始 -->
<!-- mobile导航开始 -->
<header class="mobile-header">
<nav class="navbar navbar-default " role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse"
data-target="#home-navbar">
<span class="sr-only">切换导航</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a><img class="mobile-logo" src="images/logo.png" alt="logo" title="this is a logo"></a>
</div>
<div class="collapse navbar-collapse clearfix" id="home-navbar">
<ul class="nav navbar-nav" id="mobile-nav">
<li><a href="index.html">首页</a></li>
<li><a href="solution.html">解决方案</a></li>
<li><a href="case_show.html">案例展示</a></li>
<li><a href="about_us.html">关于我们</a></li>
</ul>
</div>
</nav>
</header>
<!-- mobile导航 结束-->
<header class="pc-header">
<div class="pc-header-inner">
<a href="index.html" class="logol-link"><img class="logo" src="images/logo.png" alt="logo" title="this is a logo"></a>
<nav class="navbar navbar-default" role="navigation">
<ul class="nav navbar-nav ccl-effect-21" id="pc-nav" >
<li><a href="index.html">首页</a></li>
<li><a href="solution.html">解决方案</a></li>
<li><a href="case_show.html" class="current">案例展示</a></li>
<li><a href="about_us.html">关于我们</a></li>
</ul>
</nav>
<span class="contact-number">028-85563646</span>
</div>
</header>
<!-- 头部区域 结束 -->
</html>
<!-- 公用的导航区域是一个 html页面结束 -->
<!--在其他页面中,用一个div来加载footer页面的内容-->
<div class="loadfooter"></div>
$(".loadfooter").load("footer.html");
附上公司官网链接
在这里总结我遇到以及用到的一些效果插件
forEach不就是一个循环吗?还有什么好说的呢?当时就是因为想当然的认为,结果可想而知。。。
当时的问题场景如下:
通过银行卡的名字返回银行卡code,我想这个用循环直接搞定的,用了foreach,直接返回code,
getBankCode: function (bankName) {
this.bankInfo.bankNameList.forEach(function (item) {
if (item.name === bankName) {
return item.code;
}
})
},
但是为什么结果是undefined。。。。。而且debug的时候,不是符合条件,直接返回,为什么要一直循环完全。不是一旦符合就跳出循环。。。。这是怎么回事,有点焦灼。看来只有从源头找问题。
我们看见moz文档,看见这样的话。
There is no way to stop or break a forEach() loop other than by throwing an exception.
// 当你用foreach的时候,没有办法跳出循环,除非通过抛出一个异常(不好好看文档的我,还能说什么呢?)
If you need such behavior, the forEach() method is the wrong tool. Use a plain loop instead.
// 如果你想在循环中,符合某种条件,就跳出该循环,那么久用for循环(此时我要去看看我的foreach用没有用对,不对我还得慢慢来改啊。。。。)
If you are testing the array elements for a predicate and need a Boolean return value, you can use every() or some() instead.
// 如果您正在测试一个数组里的元素是否符合某条件,且需要返回一个布尔值,那么可使用 Array.every 或 Array.some。
If available, the new methods find() or findIndex() can be used for early termination upon true predicates as well.
如果可用,新方法 find() 或者findIndex() 也可被用于真值测试的提早终止。
// 最后,当然foreach也可以用,只不过真的不建议这种方法。。。还是用for循环代替吧。。。
getBankCode: function (bankName) {
let _code = '';
this.bankInfo.bankNameList.forEach(function (item) {
if (item.name === bankName) {
_code = item.code;
}
})
return _code;
},
在Mac下,滚动条是不占位的,所以宽度始终是为0的,在windows系统下,是占位的。
通过一个设置一个容器的高宽,以及overflow-y为scroll。获取该容器的offsetWidth与clientWidth,两着相减,就是滚动轴的宽度。
//创建元素
function getScrollBarWidth() {
var el = document.createElement('div'),
scrollBarWidth;
//设置style
el.style.width = '100px';
el.style.height = '100px';
el.style.overflowY= 'scroll';
//插入文档
document.body.appendChild(el);
//获取滚动轴宽度
scrollBarWidth = el.offsetWidth - el.clientWidth;
//移除元素兼容IE
document.body.removeChild(el);
//移除元素不兼容IE
el.renove();
return scrollBarWidth ;
}
通过一个设置一个容器的高宽,此时获取该元素的clientWidth,然后再设overflow-y为scroll的属性,再次获取该元素的clientWidth,两者相减,那么就是就是滚动轴的宽度。
//创建元素
function getScrollBarWidth() {
var el = document.createElement('div'),
scrollBarWidth;
//设置style
el.style.width = '100px';
el.style.height = '100px';
//插入文档
document.body.appendChild(el);
//获取未加入滚动轴的clientWidth
clientWidth1 = el.clientWidth;
//设置滚动轴属性
el.style.overflowY = 'scroll';
//获取加入滚动轴的clientWidth
clientWidth2 = el.clientWidth;
//获取滚动轴宽度
scrollBarWidth = clientWidth1 - clientWidth2;
//移除元素兼容IE
document.body.removeChild(el);
//移除元素不兼容IE
el.renove();
return scrollBarWidth ;
}
给一个对象设置属性并不仅仅是添加一个新属性或者修改已有的属性值。 这个是有一个过程的。
myObject.foo = 'bar';
如果myObject对象中包含名为foo的普通数据访问属性,这天赋值语句只会修改已有的属性值。
如果foo不是直接村子啊与myObject中,[[prototype]]链就会被遍历,类似[[Get]]操作。如果原型链上找不到foo,foof就会被直接添加到myObject中。
然而,如果foo存在与原型链上层,赋值语句myObject.foo = 'bar'的行为就会有些不同。
如果属性名foo既出现在myObject中也出现在myObject的[[prototype]]链的上层,那么就会发生屏蔽。
myObject中包含的foo属性会屏蔽原型链上层的所有的foo属性,因为myObject.foo总是会选择原型链最底层的foo属性。
屏蔽比我们想象的更加复杂,分析一下如果foo不直接存在与myObject中而是存在原形来链的上册myObject.foo = 'bar'会出现的三种情况。
如果在[[prototype]]链上层存在名为foo的普通数据访问属性,并且没有被标记为只读,那么会直接在myObject中添加一个名为foo的新属性,它是屏蔽属性。
如果在[[prototype]]链上层存在foo,但是它被标记为只读,那么无法修改已有的属性或者在myObject上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误,否则,这条赋值语句会被忽略。总之不会发生屏蔽。
如果在[[prototype]]链上层存在foo,并且它是一个setter那就一定会调用这个setter。foo不会添加到myObject(或者说屏蔽于),也不会重新定义foo这个setter。
所以说如果向[[prototype]]链上层添加已经存在的属性([[put]])赋值,就一定会触发屏蔽,这种说法是错误的,上面三种情况,只有一种才会实现。
如果,你希望第二种和第三种情况下也屏蔽foo,那就不能使用=操作符来赋值,而是使用Object.defineProperty(..)来向myObject添加foo.
通常来说,使用屏蔽得不偿失,所以应当尽量避免使用。
有些情况下会隐式的产生屏蔽。例如下面的这个例子
var anotherObject = {
a:2
};
var myObject = Object.create(anotherObject );
anotherObject.a; //2
myObject.a; //2
anotherObject.hasOwnProperty("a"); //true
myObject.hasOwnProperty("a"); //false
myObject.a++; //隐式的屏蔽!
anotherObject.a; //2
myObject.a; //3
myObject.hasOwnProperty("a"); //true
尽管myObject.a++看起来应该(通过委托)查找并增加anotherObject.a属性,但是别忘++操作相当于
myObject.a = myObject.a+1。因此++操作首先会通过[[prototype]]查找属性a并从anotherObject.a获取当前属性值2,然后给这个值加1,接着用[[Put]] 将值3赋给myObject中新建的屏蔽属性a。
修改委托属性时一定要小心,如果想让anotherObject.a的值增加,唯一的办法是anotherObject.a++。
实际上,javascript才是真正应该被称为“面向对象”的语言,因为它是少有的可以不通过类,直接创建对象的语言。在javascript中,类无法描述对象的行,因为根本就不存在类。对象直接定义自己的行为。javascript中只有对象。
在javascript中有一种奇怪的行为一直子啊被滥用。那就是模仿类。
这种奇怪的‘类似类’的行为利用了函数的一种特殊性质:所有的函数默认都会拥有一个名为prototype的公有且不可枚举的属性,他会指向另一个对象。
function Foo(){
//...
}
Foo.prototype; //{}
这个对象通常被称为Foo的原型,因为我们通过名为Foo.prototype的属性来访问它。然而不幸的是。这个术语对我们造成了极大的误导。
被贴上Foo.prototype标签的对象,是在调用new Foo()时创建的,最后会被关联到Foo.prototype对象上。
在javascript中,你不能创建一个类的多个实例,只能创建多个对象。他们[[prototype]]关联的是同一个对象。但是在默认情况下并不会进行复制,因此这些对象之间并不会完全失去联系。他们是互相关联的。
new Foo()会生成一个新对象,这个新对象的内容连接[[prototype]]关联的Foo.prototype对象。最后我们得到了两个对象,它们之间互相关联,就是这样。我们并没有初始化一个类,实际上我们并没有从‘类’中赋值任何到一个对象,只是让两个对象互相关联。
按照javascript的世界惯例,‘类’名首字母要大写,所以名字写作Foo而非foo似乎也提升它是一个类,这个惯例影响力非常大,以至于如果你使用new来调用小写方法或者不用new 调用大写的函数,许多javascript开发者会责怪你,这很令人吃惊,我们竟然会如此努力地维护javascript(假)‘面向类’的权力,尽管对于javascript引擎来说首字母大写没有任何意义。
在普通函数调用前面加上new关键字之后,就会把这个函数调用变成一个‘构造函数调用’。实际上,new会劫持所有普通函数并用构造对象的形式来调用它。
function nothingSpecial() {
console.log('don`t mind me');
}
var a = new nothingSpecial();
a; //
nothingSpecial只是一个普通的函数,但是使用new调用时,它会构造一个对象并赋值给a,这看起来像是new的一个副作用(无论如何都会构造一个对象)。这个调用是一个构造函数的调用,但是nothingSpecial本身并不是一个构造函数。
在javascript中对于‘构造函数’最准确的解释是,所有带new的函数调用。函数不是构造函数,但是当且仅当使用new时,函数调用会变成‘构造函数调用’。
function Foo(name) = {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
};
function Bar(name, label) {
Foo.call(this, name);
this.label = label;
}
//我们创建一个新的Bar.prototype对象并关联到Foo.prototype
Bar.prototype = Object.create(Foo.prototype);
//注意,现在没有Bar.prototype.constructor了
//如果你需要这个属性的话可能需要手动修复一下它
Bar.prototype.myLable = function() {
return this.label;
}
var a = new Bar('a', 'obj a');
a.myName(); //'a'
a.myLable(); //'obj a'
这段代码的核心是,Bar.prototype = Object.create(Foo.prototype)。调用Object.create(...)会凭空创建一个‘新’对象并把新对象内部的[[prototype]]关联到你指定的对象。例如本例的(Foo.prototype)。即创建一个新的Bar.prototype对象并把它关联到Foo.prototype。
声明function Bar() {..}时,和其他函数一样,Bar会有一个.prototype关联到默认的对象,但是这个对象并不是我们想要的Foo.prototype。因此我们创建了一个新对象并把它关联到我们希望的对象上,直接把原始的关联对象抛弃掉。
注意,下面两种方式是常见的错误做法,实际上他们都存在一些问题
//和你想要的机制不一样
Bar.prototype = Foo.prototype;
//基本上满足你的需求,但是可能会产生一些副作用
Bar.prototype = new Foo();
Bar.prototype = Foo.prototype
并不会创建一个关联到Bar.prototype的新对象,它只Bar.prototype直接引用Foo.prototype对象,因此当你执行类似的Bar.prototype.myLabel = ...的赋值语句时会直接修改
Foo.prototype对象本身。显然这不是你想要的结果。否则你根本不需要Bar对象,直接修改Foo就可以了。
Bar.prototype = new Foo();的确会创建一个关联的Bar.prototype的新对象,但是它使用了Foo(..)的构造函数调用,如果函数Foo有一些副作用(比如修改状态...)的话,就会影响到Bar()的后代,后果不堪设想。
如果能有一个标准并且可靠的方法来修改对象的[[prototype]]关联就好了,造ES6之前,我们只能通过设置.__proto__属性来实现,但是这个方法并不是标准的并且无法兼容所有浏览器。ES6添加了辅助函数Object.setPrototypeOf(..)可以用标准并且可靠的方法来修改关联。
//ES6之前需要抛弃默认的Bar.prototype
Bar.prototype = Object.create(Foo.prototype);
//ES6开始可以直接修改现有的Bar.prototype
Object.setPrototypeOf(Bar.prototype, Foo.prototype);
如果忽略掉Object.create(...)方法带来的轻微性能损失(抛弃的随性需要进行垃圾回收)它实际上比ES6及其之后的方法更短而且可读性更高。
var foo = {
something: function() {
console.log("Tell me something good...");
}
};
var bar = Object.create(foo);
bar.something(); //Tell me something good...
Object.create(...)会创建一个新对象bar并把它关联到我们指定的对象foo,这样我么就可以充分发挥[[Prototype]]机制的威力(委托)并且避免不必要的麻烦(比如使用new的构造函数调用会生成.prototype和.constructor)
Object.create(null)
上面代码会创建一个拥有空(或者说null)[[Prototype]]连接的对象,这个对象无法进行委托。由于这个对象没有原型链,所以instanceof操作符无法进行判断,因此总是会返回false。这些特殊的空的[[Prototype]]对象通常被称为字典,他们完全不会受到圆形链的干扰,因此非常适合用来存储数据。
我们不需要用类来创建两个对象之间的关系,只需通过委托来关联对象就足够了。Object.create(...)不包含任何类。所以它可以较好的创建我们想要的关联关系。
function Foo() {
this.me = who;
}
Foo.prototype.identify = function() {
return 'I am '+ this.me;
}
function Bar(who) {
Foo.call(this, who);
}
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.speak = function() {
alert('hello, '+ this.identify() + '.');
};
var b1 = new Bar('b1');
var b2 = new Bar('b2');
b1.speak();
b2.speak();
子类Bar继承了父类Foo,然后生成b1和b2的两个实例。b1委托了Bar.prototype。后者委托了Foo.prototype。这种风格很常见。
下图是类风格代码的思维模式强调实体以及实体间的关系。
从图中可以看出这是一张复杂的关系网。此外,如果你跟着图中的箭头走就会发现,javascript机制有很强的内部连贯性。
例如:javascript中的函数之所以可以访问call(..),apply(..)和bind(..),就因为函数本身就是对象。而函数对象同样有[[prototype]]属性并且关联到Function.prototype对象。因此所有函数对象都可以通过委托调用这些默认方法。
Foo = {
init: function(who) {
this.me = who;
},
identify: function() {
return 'i am '+ this.me
}
};
Bar = Object.create(Foo);
Bar.speak = function() {
alert('hello, '+ this.identify() + '.');
}
var b1 = Object.create(Bar);
b1.init('b1')l
var b2 = Object.create(Bar);
b2.init('b2');
b1.speak();
b2.speak();
在这段代码中同样利用了[[prototype]]把b1委托给Bar并把Bar委托给Foo,和上一段代码一模一样。我们仍然实现了三个对象之间的关联。在这段代码中,我们只是把对象关联起来了,并不需要那些复杂又令人困惑的模仿类的行为(构造函数,原型以及new)。
下图是对象关联风格代码的思维模型。
在 CORS 出现之前,要实现 Ajax 跨域通信是比较困难的。开发人员们想出了一些方法,利用 DOM 中能够执行跨域请求的功能,在不依赖 XHR 对象的情况下也能发送某种请求。
我们知道一个网页可以从任何网页中加载图像。不用担心跨不跨域。可以动态地创建图像,使用他们的onload和onerror事件处理程序来确定是否接受到了响应。
动态创建图像经常用于图像Ping。图像Ping是与服务器进行简单、单向的跨域通信的一种方式。请求的数据是通过查询字符串的形式发送的、而响应可以是任意内容,但通常是像素图或者204响应。通过图像Ping,浏览器得不到任何具体的数据,但通过侦听load和error事件,它能知道响应是什么时候接收到的。
var img = new Image();
img.onload = img.onerror = function() {
alert("done!");
var imgBox= document.getElementById("img-box");
imgBox.appendChild(img);
}
img.src='http://www.xxx.com/test?name=nicko';
这这里我们创建了一个Image的实例,然后在onload 和onerror 事件处理程序指定为同一个函数。这样无论是什么响应,只要请求完成,就能得到通知。
请求从设置src属性你那一刻开始的。即图片的地址。
图像Ping最常用于跟踪用户点击页面或动态的曝光次数。但是他有两个缺点
图像Ping只能用于浏览器与服务器间的单向通信。
JSONP是JSON with padding(填充式JSON或者参数式JSON) 的简写,是应用JSON的一种新方法。
JSONP看起来与JSON差不多,只不过被包含在函数调用中的JSON。例如:
callback({'name': 'mike'})
;
JSONP由两部分组成:回调函数和数据。回调函数是当响应到来时,应该在页面中调用的函数。回调函数的名字一般是在请求中的指定的。而数据就是传入回调函数中的JSON数据。
http://xx.net/json/?callback=handleResponse
上面这个例子,其中URL是在请求一个JSONP地理定位服务。通过查询字符串来指定JSONP服务的回调参数是很常见的,这里指定的回调函数的名字叫handleResponse()。
JSONP是通过动态的<script>
元素来使用的,使用时可以为其src属性指定一个跨域的URL。这里的<script>
元素和<img>
元素类似,都有能力不受限制地从其他域加载资源。因为JSONP是有效的javascript代码,所以在请求完成后,即在JSONP响应加载到页面中以后,会立即执行。
function handleResponse(response) {
alert("you`re at IP address" + response.ip);
}
var script = document.createElement('script');
script.src = 'http://xx.net/json/?callback=handleResponse';
docuemnt.body.insertBefore(script, document.body.firstChild);
这个例子通过查询地理定位服务来显示你的IP地址。
JSONP非常流行,在于其很容易。与图像Ping相比,他的优点在于
* 能够直接访问文本
* 支持浏览器与服务器的双向通信
但是他有他的缺点:
* JSONP是从其他域加载代码执行,如果其他域不安全的话,很可能夹带一些恶意代码。
* 确定请求是否失败并不容易,如果动态脚本插入有效,就执行调用;如果无效,就静默失败。失败是没有任何提示的。例如,不能从服务器捕捉到 404 错误,也不能取消或重新开始请求。不过,等待一段时间还没有响应的话,就不用理它了。(未来的 jQuery 版本可能有终止 JSONP 请求的特性)。
看一个例子,在A域名下,请求B域名下资源。
<script type="text/javascript">
function addScriptTag(src){
var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.src = src;
document.body.appendChild(script);
}
window.onload = function(){
//调用远程服务
addScriptTag("http://localhost:20002/MyService.ashx?callback=person");
}
//回调函数person
function person(data) {
alert(data.name + " is a " + data.sex);
}
</script>
Ajax是一种页面向服务器请求数据的技术,而Comet则是一种服务器向页面推送数据的技术。Comet能够让信息近乎实时地被推送到页面上,非常适合处理体育比赛的分数和股票报价。
Comet的实现方式:
无论是短轮询还是长轮询,浏览器都要在接受数据之前,先发起对服务器的连接。这两者最大的区别在于服务器如何发送数据。短轮询是服务器立即发送响应,无论数据是否有效,而长轮询是等待发送响应,轮询的优势在于所有浏览器都支持,因为使用XHR对象和setTimeout()就能实现。
例如下面这段PHP脚本就是采用流实现的服务器中常见的形式。
<?php
$i = 0;
while(true) {
//输出一些数据,然后理解刷新输出缓存
echo 'Number is $i';
flush();
//等几秒中
sleep(10)
$i++
}
所有服务器语言都支持打印到输出缓存然后刷新(将输出缓存中的内容一次性全部发送到客户端)的功能。这正是实现HTTP流的关键所在。
在浏览器中,通过侦听readystatechange事件以及检测readyState的值是否为3,就可以利用XHR对象实现HTTP流。随着不断从服务器接受数据,readyState的值会周期性的变为3。当readyState的值为3时,responeseText属性中会保存接受到的数据,此时就需要比较接受到的数据,决定从什么位置开始取得最新的数据。
function createStreamingClient(url, progress, finished){
var xhr = new XMLHttpRequest(),
received =0;
xhr.open('get', url, true);
xhr.onreadystatechange = function() {
var result;
if(xhr.readyState == 3){
//只取得最新数据并调整计数器
result = xhr.responseText.substring(received);
received += result.length;
//调用progress回调函数
progress(result);
}else if(xhr.readyState == 4){
finished(xhr.responseText);
}
};
xhr.send(null);
return xhr;
}
var client = createStreamingClient('streaming.php', function(data){
alert('received:'+ data);
}, function(data) {
alert('done');
});
在这个createStreamingClient函数接受三个参数,要连接的URL、在接受到数据时调用的函数、以及关闭连接时调用的函数。有时候,当连接关闭时,很可能需要重新建立,所以关注连接什么时候关闭还是很有必要的。
只要readystatechange事件发生,而且readyState的值为3,就对responseText进行分割以取得最新数据,这里的received变量用于记录以及处理多少个字符,每次readyState的值为3时都以递增,然后,通过progress回调函数来处理传入的新数据,而的那个readyState的值为4时,则执行Finnish回调函数,传入响应返回的全部内容。
管理Comet的连接是很容易出错的,需要时间不断改进才能达到完美。
SSE(Server-Sent Events, 服务器发送事件)是围绕只读Comet交互推出的API或者模式,SSE API用于创建到服务器的单向连接,服务器通过这个连接可以发送任意数量的数据。服务器响应的MIME类型必须是
text/event-strem
而且是在浏览器中JavaScript API能解析可是输出。SSE支持短轮询、长轮询、HTTP流、而且能在断开连接是自动确定何时重新连接。用这个的话,实现Comet就容易很多了。
SSE的JavaScript API与其他传递消息的JavaScript API很相似。要预定新的事件流,首先要创建一个显得EventSource对象,并传进一个入口点
var source = new EventSource('myevents.php');
注意,传入的URL必须与创建对象的页面同源。EvnetSource的实例有一个readyState属性,值为0表示正在连接服务器,值为1表示打开了连接,值为2表示关闭了连接。
还有3个事件。
就一般的用法而言,onmessage事件处理程序也没有什么特别的。
source.onmessage = function(event) {
var data = event,data;
}
服务器发回的数据以字符串形式保存在event.data中。
默认的情况下,EventSource对象会保持与服务器的活动连接,如果连接断开,还会重新连接。这意味着SSE适合长轮询和HTTP流。如果想强制立即断开连接并且不再重新连接,可以调用close()方法。
source.close()
所谓的服务器事件会通过一个持久的HTTP响应发送,这个响应的MIME类型为text/event-strem
.响应的格式是纯文本,最简单的情况是每个数据项都带有前缀data:,例如:
data: foo
data: bar
data: foo
对于以上响应而言,事件流中第一个message事件返回的event.data的值为foo,第二个message事件返回event.data值为bar...对于多个连续的以data:开头的数据行,将作为多段数据解析,每个值之前以一个换行符分隔。只有在包含data:的数据行后面有空行,才会触发message事件。因此在服务器上生产事件流是不能忘了多添加一行。
通过id:前缀可以给特定的事件指定一个关联的ID,这个ID行位于data:行前面或者后面皆可:
data: foo
id: 1
设置ID后,EventSource对象会跟踪上一次触发的事件。如果连接断开,会向服务器发送一个包含名为Last-Event-id特殊HTTP头部的请求,以便服务器知道下一次该触发那个事件。在多次连接事件流中,这种机制可以确保浏览器以正确的顺序收到连接的数据段。
Web Sockets的目标是在一个单独的持久连接上提供全双工、双向通信。在JavaScript中创建了 Web Sockets之后,会有一个HTTP请求发送到浏览器以发送连接。在取得服务器响应后,建立的连接会使用HTTP升级从HTTP协议交换为Web Socket协议。也就是说,使用标准的HTTP服务器无法实现Web Sockets,只有支持这种协议的专门服务器才能正常工作。
由于 Web Sockets使用了自定义的协议,所以URL模式也略有不同。未加密的连接不再是http://
而是ws://
加密连接不再是https://
而是wss://
在使用Web Sockets URL时,必须带有这个模式,因为将来有可能支持其他模式。
使用自定义协议而非HTTP协议的好处:
使用自定义协议而非HTTP协议的缺点:
要创建Web Socket,先实例一个Web Socket对象并传入要连接的URL
var socket = new WebSocket('ws://www.example.com/server.php');
注意,必须给WebSocket构造函数传递绝对的URL,同源策略对Web Socket不适用。因此可以通过它打开任何站点的连接。至于是否会在某个域中的页面通信,则完全取决于服务器。
实例化WebSocket对象后,浏览器就会马上尝试创建连接,与XHR类似,WebSocket也有一个表示当前状态的readyState属性,不过,这个属性与XHR并不相同。而是如下所示:
WebSocket没有readystatechange事件,不过,它有其他事件,对应着不同的状态。redayState的值永远从0开始。
要关闭WebSocket连接,可以在任何时候调用close()方法。
socket.close()
调用close()之后,readyState的值立即变成2(正在关闭)。而在关闭连接后就会变成3
WebSocket打开之后,就可以通过连接发送和接受数据。要向服务器发送数据,使用send()方法并传入任意字符串。例如:
var socket = new WebSocket('ws://www.example.com/server.php');
socket.send('hello world');
因为Web Socket只能通过连接发送纯文本数据,所以对于复杂的数据结构,在通过连接发送之前,必须进行序列化,例如:
var message = {
time: new Date(),
text: 'hello world',
clientId: 'asd0238'
};
socket.send(JSON.stringify(messsge));
接下来,服务器要读取其中的数据,就要解析接受到的JSON字符串。
当服务器想客户端发来消息时, WebSocket对象就会触发message事件。这个message事件与其他传递消息协议类似,也是把返回的数据保存在event.data属性中。
socket.onmessage = function(event) {
var data = event.data;
//处理数据
}
通过send()发送到服务器的数据一样,event.data中返回的数据也是字符串。如果你想得到其他格式的数据,必须手工解析这些数据。
WebSocket对象还有其他三个事件,在连接生命周期的不同阶段触发。
WebSocket对象不支持DOM2级事件侦听器,因此必须使用DOM0D级语法分别定义每个事件处理程序。
var socket = new WebSocket('ws://www.example.com/server.php');
socket .onopen = function() {
alert("Connect established");
}
socket.onerror = function() {
alert("Connect error");
}
socket.onclose = function() {
alert("Connect closed");
}
在这三个事件中,只有close事件的event对象有额外的信息,这个事件的事件对象有三个额外属性
wasClean
code
reason
。其中,wasClean
是一个布尔值,表示连接是否已经明确地关闭。code
是服务器返回的数值状态码。 reason
是一个字符串,包含服务器发回的消息。可以把信息给用户,也可以记录到日志中以便将来分析
socket.onclose = function(event) {
console.log('was clean?'+ event.wasClean + 'code=' + event.code + 'reason=' + event.reason);
}
用SSE还是Web Sockets呢?可以从下面几个方面考虑:
你是否有自由度建立和维护Web Sockets的服务器?而SSE是通过常规的HTTP通信,现有的服务器就可以满足需求。
到底需不需要双向通信。如果只需要读取服务器数据(如比赛成绩),那么SSE比较容易实现,如果必须双向通信(例如聊天室)那么Web Sockets显然更好。
在不能选择Web Sockets的情况下,组合XHR和SSE也能实现双向通信。
对于未被授权系统有权访问某个资源的情况,我们称之为CSRF(Cross-Site Request Forgery,跨站点请求伪造)。未被授权系统会伪装自己,让处理的请求服务器认为它是合法的。受到CSRF攻击的Ajax程序有大有小,攻击行为既有旨在揭示系统漏洞的恶作剧,也有恶意的数据窃取和数据销毁。
为了确保通过XHR访问的URL安全,通常的做法就是验证发送的请求者是否有权限访问相应的资源。有下列几种方式可供选择。
请注意,下列措施对防范CSRF攻击不起作用
XHR对象也提供一些安全机制,虽然表面上可以保证安全,但实际上却相当不可靠。实际上,open方法还能再接受两个参数: 要随请求一起发送的用户名和密码。带有这两个参数的请求可以通过SSL发送给服务器上的页面。例如:
xhr.open('get', 'example.php', true, 'username', 'password'); //不要这样做
因为只要打开控制器就能发现纯文本的用户名和密码。
对于attributes和properties这两个。经常弄混淆。所以想总结一下。
文章链接1
stackoverflow关于这个问题的讨论
文章链接2
每一个DOM节点,都是一个对象。像其他JS对象一样,DOM节点这类型HTMLElement对象,也可以添加一些方法或者属性。
这些自定义添加的属性,就是property。它只能被JS所读取,并不会影响HTML的展示。(它能被JS的for-in方法遍历出来,但innerHTML里面不会显示)
与Property不同,Attribute会DOM节点上显示出来,但不会在DOM对象中被for-in遍历出来。
例如上图的input标签,他的Attributies包括value,type,name,id。
想操作DOM元素的的attribute,得依靠下列的JS方法:
elem.hasAttribute(name);// 判断是否存在
elem.getAttribute(name);// 获取该Attribute的值
elem.setAttribute(name, value);// 写入该Attribute的值
elem.removeAttribute(name);// 删除该Attribute
需要注意的是
并不是所有的attribute与对应的property名字都一致,比如attribute 的class属性,使用property操作的时候应该是这样className
对于值是true/false的property,类似于input的checked attribute等,attribute取得值是HTML文档字面量值,property是取得计算结果,property改变并不影响attribute字面量,但attribute改变会一向property计算
<input id="test3" type="checkbox"/>
var t=document.getElementById('test3');
console.log(t.getAttribute('checked'));//null
console.log(t.checked);//false;
t.setAttribute('checked','checked');
console.log(t.getAttribute('checked'));//checked
console.log(t.checked);//true
t.checked=false;
console.log(t.getAttribute('checked'));//checked
console.log(t.checked);//false
<a id="test4" href="#">Click</a>
var t=document.getElementById('test4');
console.log(t.getAttribute('href'));//#
console.log(t.href);//file:///C:/Users/bsun/Desktop/ss/anonymous.html#
然而关于checked 属性需要记住的最重要的一点是:它和checked property并不是一致的。实际上这个attribute和defaultChecked property一致,而且只应该用来设置checkbox的初始值。checked attribute并不随着checkedbox的状态而改变,但是checked property却跟着变。因此浏览器兼容的判断checkebox是否被选中应该使用property
if ( elem.checked )
if ( $( elem ).prop( "checked" ) )
if ( $( elem ).is( ":checked" ) )
这对其它一些类似于selected、value这样的动态attribute也适用。
所有的DOM节点对象,都有一套标准的properties 。这些DOM对象的标准properties,会自动与其attributes同步。例如id\title\lang\dir\className
document.body.setAttribute('id','la-la-la'):
alert(document.body.id); // la-la-la
但有的properties与attributes的同步值,却并不是一样的
自动同步,但值并不完全相等。例如表单元素input的checked属性。
例如:表单元素input的value属性。
从Attribute同步到Property
Property却不能同步到Attribute
Properties就是JavaScript对象中的一个属性,而Attribute则是HTML元素中的一个属性。
Properties | Attribute |
---|---|
值可以任意类型的值 | 值只能是字符串 |
键名区分大小写 | 键名不区分大小写 |
在innerHTML里面不可见 | 在innerHTML里面可见 |
标准的DOM Properties会自动与Attributies同步,自定义的则不会
最近在看HTML5的拖拽上传,看了Firefox的文档,跟着做了一个demo,其中涉及相关方向的知识,发现有些知识以前没有涉及到,特上网搜索相关知识后,补充一下。
xhr提供了2个用来获取响应头部的方法:getAllResponseHeaders和getResponseHeader。
前者是获取 response 中的所有header 字段,后者只是获取某个指定 header 字段的值。另外,getResponseHeader(header)的header参数不区分大小写。
这两个方法有如下可能出现的问题?
原因1:W3C的 xhr 标准中做了限制,规定客户端无法获取 response 中的 Set-Cookie、Set-Cookie2这2个字段,无论是同域还是跨域请求;
原因2:W3C 的 cors 标准对于跨域请求也做了限制,规定对于跨域请求,客户端允许获取的response header字段只限于“simple response header”和“Access-Control-Expose-Headers” (两个名词的解释见下方)。
"simple response header"
"simple response header"包括的 header 字段有:Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma;
"Access-Control-Expose-Headers"
"Access-Control-Expose-Headers":首先得注意是"Access-Control-Expose-Headers"进行跨域请求时响应头部中的一个字段,对于同域请求,响应头部是没有这个字段的。这个字段中列举的 header 字段就是服务器允许暴露给客户端访问的字段。
所以getAllResponseHeaders()只能拿到限制以外(即被视为safe)的header字段,而不是全部字段;而调用getResponseHeader(header)方法时,header参数必须是限制以外的header字段,否则调用就会报Refused to get unsafe header的错误。
有些时候我们希望xhr.response返回的就是我们想要的数据类型。比如:响应返回的数据是纯JSON字符串,但我们期望最终通过xhr.response拿到的直接就是一个 js 对象,我们该怎么实现呢?
有2种方法可以实现,一个是level 1就提供的overrideMimeType()方法,另一个是level 2才提供的xhr.responseType属性。
xhr.overrideMimeType()
overrideMimeType是xhr level 1就有的方法,所以浏览器兼容性良好。这个方法的作用就是用来重写response的content-type,这样做有什么意义呢?
比如:server 端给客户端返回了一份document或者是 xml文档,我们希望最终通过xhr.response拿到的就是一个DOM对象,那么就可以用xhr.overrideMimeType('text/xml; charset = utf-8')来实现。
再举一个使用场景,我们都知道xhr level 1不支持直接传输blob二进制数据,那如果真要传输 blob 该怎么办呢?当时就是利用overrideMimeType方法来解决这个问题的。
responseType是xhr level 2新增的属性,用来指定xhr.response的数据类型,目前还存在些兼容性问题。那么responseType可以设置为哪些格式呢,如下:
值 | xhr.response 数据类型 | 说明 |
---|---|---|
"" | String字符串 | 默认值(在不设置responseType时) |
"text" | String字符串 | |
"document" | Document对象 | 希望返回 XML 格式数据时使用 |
"json" | javascript 对象 | 存在兼容性问题,IE10/IE11不支持 |
"blob" | Blob对象 | |
"arrayBuffer" | ArrayBuffer对象 |
虽然在xhr level 2中,2者是共同存在的。但其实不难发现,xhr.responseType就是用来取代xhr.overrideMimeType()的,xhr.responseType功能强大的多,xhr.overrideMimeType()能做到的xhr.responseType都能做到。所以我们现在完全可以摒弃使用xhr.overrideMimeType()了。
在上传或者下载比较大的文件时,实时显示当前的上传、下载进度是很普遍的产品需求。
我们可以通过onprogress事件来实时显示进度,默认情况下这个事件每50ms触发一次。需要注意的是,上传过程和下载过程触发的是不同对象的onprogress事件:
xhr.onprogress = updateProgress;
xhr.upload.onprogress = updateProgress;
function updateProgress(event) {
//进度是否可用的布尔值
if (event.lengthComputable) {
//loaded为上传或下载的大小/total为全部的大小
var completedPercent = event.loaded / event.total;
}
}
interface XMLHttpRequestEventTarget : EventTarget {
// event handlers
attribute EventHandler onloadstart;
attribute EventHandler onprogress;
attribute EventHandler onabort;
attribute EventHandler onerror;
attribute EventHandler onload;
attribute EventHandler ontimeout;
attribute EventHandler onloadend;
};
interface XMLHttpRequestUpload : XMLHttpRequestEventTarget {
};
interface XMLHttpRequest : XMLHttpRequestEventTarget {
// event handler
attribute EventHandler onreadystatechange;
readonly attribute XMLHttpRequestUpload upload;
};
XMLHttpRequestEventTarget接口定义了7个事件:
每一个XMLHttpRequest里面都有一个upload属性,而upload是一个XMLHttpRequestUpload对象
XMLHttpRequest和XMLHttpRequestUpload都继承了同一个XMLHttpRequestEventTarget接口,所以xhr和xhr.upload都有第一条列举的7个事件
onreadystatechange是XMLHttpRequest独有的事件
所以这么一看就很清晰了:
xhr一共有8个相关事件:7个XMLHttpRequestEventTarget事件+1个独有的onreadystatechange事件;而xhr.upload只有7个XMLHttpRequestEventTarget事件。
当请求一切正常时,相关的事件触发顺序如下:
触发xhr.onreadystatechange(之后每次readyState变化时,都会触发一次)
触发xhr.onloadstart
//上传阶段开始:
触发xhr.upload.onloadstart
触发xhr.upload.onprogress
触发xhr.upload.onload
触发xhr.upload.onloadend
//上传结束,下载阶段开始:
触发xhr.onprogress
触发xhr.onload
触发xhr.onloadend
发生abort/timeout/error异常的处理
在请求的过程中,有可能发生 abort/timeout/error这3种异常。那么一旦发生这些异常,xhr后续会进行哪些处理呢?后续处理如下:
一旦发生abort或timeout或error异常,先立即中止当前请求
将 readystate 置为4,并触发 xhr.onreadystatechange事件
如果上传阶段还没有结束,则依次触发以下事件:
xhr.upload.onprogress
xhr.upload.[onabort或ontimeout或onerror]
xhr.upload.onloadend
触发 xhr.onprogress事件
触发 xhr.[onabort或ontimeout或onerror]事件
触发xhr.onloadend 事件
在哪个xhr事件中注册成功回调?
从上面介绍的事件中,可以知道若xhr请求成功,就会触发xhr.onreadystatechange和xhr.onload两个事件。 那么我们到底要将成功回调注册在哪个事件中呢?我倾向于 xhr.onload事件,因为xhr.onreadystatechange是每次xhr.readyState变化时都会触发,而不是xhr.readyState=4时才触发。
xhr.onload = function () {
//如果请求成功
if(xhr.status == 200){
//do successCallback
}
}
上面的示例代码是很常见的写法:先判断http状态码是否是200,如果是,则认为请求是成功的,接着执行成功回调。这样的判断是有坑儿的,比如当返回的http状态码不是200,而是201时,请求虽然也是成功的,但并没有执行成功回调逻辑。所以更靠谱的判断方法应该是:当http状态码为2xx或304时才认为成功。
xhr.onload = function () {
//如果请求成功
if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
//do successCallback
}
}
在读得模式下,改属性会返回与调用元素的所有的子节点(包括元素,文本节点,以及注释)对应的HTML标记。
<div id="container">
<p>Lorem ipsum dolor sit amet, consectetur </p>
<ul>
<li>Lorem ipsum dolor</li>
<li>Lorem ipsum dolor</li>
<li>Lorem ipsum dolor</li>
<li>Lorem ipsum dolor</li>
</ul>
</div>
var box = document.getElementById("container");
console.log(box.innerHTML);
/*这个会返回如下:
<p>Lorem ipsum dolor sit amet, consectetur </p>
<ul>
<li>Lorem ipsum dolor</li>
<li>Lorem ipsum dolor</li>
<li>Lorem ipsum dolor</li>
<li>Lorem ipsum dolor</li>
</ul>
*/
对于使用innerHTML属性返回的文格式对于各个浏览器来说,是不同的。IE,Opera会把所有的标签名转为大写的形式,而chrome与Firefox则会原原本本返回,包括缩进和空格。
在写的模式下,innerHTML的值会被解析为DOM子树。替换掉调用元素原来的所有子节点。
bxo.innerHTML = "hello, <b>ann</b>"
<!-- 设置innerHTML后的结果-->
<div id="container">hello, <b>ann</b></div>
该方法接受两个参数,一个插入的位置,一个要插入的HTML的文本。
第一个参数必须是下列值之一:
var div = document.getElementById("div");
//取得class的name;
console.log(div.getAttibute("class"));
<div hello-suan="hello" id="div">hello</div>
var div = document.getElementById("div");
console.log(div.getAttribute("hello-suan"));
//设置特性
div.setAttribute("id","hello");
//添加一个自定义属性
div.hello-suan = "hello";
//并不会成为元素的特性
console.log(div.getAttribute("hello-suan"));
//放回null
改方法是彻底删除元素的特性,该方法不仅会清除特性值,也会从元素中完全删除特性。
//设置特性
div.removeAttribute("id");
判断添加当前两者的高度大于后面一项的时候,内容区域就进入了可视区域。
//当前窗口的可视区域的高度的获取。
window.innerHeight //第一种获取方式
document.documentElement.clientHeight || document.body.clientHeight
//在IE7兼容模式下,用document.body.clientHeight获取,在其他用
// document.documentElement.clientHeight 获取。
//文档被卷曲的高度scrollTop
document.body.scrollTop
//内容区域偏移量offsetTop,这个存在兼容问题。不同的浏览器的理解是不相同的。
注意,我就犯了一个错误,就是在滑动的是即onscroll事件里面scrollTop的数值一直在变化,但是我写出的一直都没有发生变化,原来是因为我没有吧这个值放在这个事件里面,放在window.onload事件里面了。所以出错了。
看看一个demo
<p id="content">Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Dolore ipsa, illo nesciunt porro sed repellat quibusdam aliquam nobis harum
officiis aperiam eum possimus adipisci officia expedita beatae omnis enim,
obcaecati!
</p>
p {
margin: 1000px auto 0;
width: 400px;
height: 400px;
background-color: #ccc;
}
window.onload = function() {
var viewH = document.documentElement.clientHeight || document.body.clientHeight;
var content = document.getElementById('content');
//content.offsetTop是固定的值是该元素的偏移量
window.onscroll = function() {
// 当窗口开始滚动时。
var sTop = document.body.scrollTop ||document.documentElement.scrollTop;
console.log(viewH+sTop);
console.log(content.offsetTop);
if ((viewH+sTop) > content.offsetTop ){
alert('进入到可视区域内了');
}
}
}
getBoundingClientRect();该方法获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置,他返回的是一个对象,即Object,该对象有是个属性:top,left,right,bottom。以像素为单位。
兼容性:上面四个属性,在ie5以上都能支持。而width和height这两个属性:ie9以上支持width/height属性。如果要兼容这两属性的话,可以采用下面的兼容形式
var rectWidth = rectObject.right - rectObject.left;
rectHeight = rectObject.bottom - rectObject.top;
在ie7及ie7以下left和top会多出两个像素的原因?
这是因为,在ie7及ie7以下的html元素坐标会从(2, 2)开始算起,在ie8已经修复了这个bug。这就是多出两个像素的原因。
var rectLeft = rectObject.left - document.documentElement.clientLeft || 2,
rectRight = rectObject.right - document.documentElement.clientLeft || 2,
rectBottom = rectObject.bottom - document.documentElement.clientTop || 2,
rectTop = rectObject.top - document.documentElement.clientTop || 2,
上面还可以用这种方式做。即这个元素的 getBoundingClientRect().top的高度小于可视区域的高度,那么就说明该元素出现在可视区域里面了。
window.onload = function() {
var viewH = document.documentElement.clientHeight || document.body.clientHeight;
var content = document.getElementById('content');
window.onscroll = function() {
var contentTop =content.getBoundingClientRect().top
//在滑动的过程中,这个元素的相对于浏览器窗口的位置一直在变化。
if (contentTop <= viewH){
alert('进入到可视区域内了');
}
}
}
在jquery中有一个方法is(":visible")
来判断元素是否可见,此时根据上面所学的,我们可以根据上面的写出相同功能的函数,而且性能还高。
function isVisible(ele) {
//兼容低版本浏览器
var h= ele.getBoundingClientRect().bottom-ele.getBoundingClientRect().top;
//不兼容ie9及以下浏览器
var h= ele.getBoundingClientRect().height;
if( h=== 0 ){
alert('该元素不可见');
}else {
alert('该元素可见');
}
}
在这里先要区分两个概念display:none
和visibility:hidden
以及opacity: 0
display:none
不为被隐藏的对象保留其物理空间,即该对象在页面上彻底消失,通俗来说就是看不见也摸不到。
visibility:hidden
和opacity: 0
使对象在网页上不可见,但该对象在网页上所占的空间没有改变,通俗来说就是看不见但摸得到。
当我们判断一个元素是否可见:是说明一个元素在文档中占位的空间有大于0的高度和宽度的。当你给一个元素设置高和宽以及visibility:hidden
,此时该元素还是占位的,说明该元素是可见的。这里的可见的判断标准,是该元素在文档中占位,不可见是该元素在文档中是不占位的。
有了这个方法,获取元素在页面的位置就更方便了
var X= this.getBoundingClientRect().left+document.documentElement.scrollLeft;
var Y =this.getBoundingClientRect().top+document.documentElement.scrollTop;
getClientRects方法返回的TextRectangle对象。W3C提供了一个文本的TextRectangle 对象,这个对象是对文本区域的一个解释。这里的文本区域只针对inline 元素,比如:a, span, em这类标签元素。
getClientRects 返回一个TextRectangle集合,就是TextRectangleList对象。getBoundingClientRect 返回 一个TextRectangle对象。
getClientRects返回的其实是个数组,数组中有很多个类似getBoundingClientRect返回的对象。getBoundingClientRect返回的永远是最外框框的那个矩形区域相关的坐标偏移对象;而getClientRects是多行文字区域的坐标偏移集合,在非IE浏览器下,只对inline的标签有反应。
{
top : (number)
bottom : (number)
left : (number)
right : (number)
width : (number)
height : (number)
}
类似新浪微博的用户信息卡的功能
<p id="content">
hello world <span id="span" class="span">Lorem ipsum dolor sit amet,
consectetur adipisicing elit. Sed dolor ipsa totam accusantium nulla ad dolorum?
Voluptate natus saepe itaque maxime consequatur libero excepturi voluptas
quisquam, odit, voluptatum tenetur incidunt!</span>
hello world
</p>
在对于span的内联元素,在小屏幕下,内容会被分成好几行。getClientRects 该方法就生成几个元素的集合。即有几个矩形。
* {
padding: 0;
margin: 0;
}
span {
background-color: #999;
cursor: pointer;
}
var content = document.getElementById('content');
var span = document.getElementById('span');
span.onclick = function(event) {
var mouseY = event.clientY;
var objRectList = this.getClientRects();
var len = objRectList.length;
for (var i = 0; i < len; i++) {
//根据高度来判断点击了哪一行,这样我们就可以通过鼠标移入或者点击的坐标来决定卡片显示的位置
if ((objRectList[i].top <= mouseY) && (mouseY<= objRectList[i].bottom)){
alert('你点击了第' + (i+1) + '行');
}
}
}
鼠标相对于浏览器窗口可视区域的X,Y坐标(窗口坐标),可视区域不包括工具栏和滚动条。IE事件和标准事件都定义了这2个属性.IE下(7及以下的浏览器)此属性不规范,它们的最小值不是0而是2,因为它是以(2,2)为坐标原点。
鼠标相对于整个页面的X/Y坐标。注意,整个页面的意思就是你整个网页的全部,比如说网页很宽很长,宽2000px,高3000px,那pageX,pageY的最大值就是它们了。类似于event.clientX、event.clientY,但它们使用的是文档坐标而非窗口坐标
兼容性:除IE6/7/8不支持外,其余浏览器均支持
offsetX/Y获取到是触发点相对被触发dom的左上角距离,不过左上角基准点在不同浏览器中有区别,其中在IE中以内容区左上角为基准点不包括边框,如果触发点在上边框会返回负值,而chrome中以边框左上角为基准点
兼容性:IE6/7/8不支持外。IE9及以上触发点在上边框会返回负值。Firefox的结果也是跟IE一样的。
chrome以边框左上角为基准点。
鼠标相对于用户显示器屏幕左上角的X,Y坐标。
layerX/Y获取到的是触发点相对被触发dom左上角的距离,数值与offsetX/Y相同,这个变量就是firefox用来替代offsetX/Y的,基准点为边框左上角,但是有个条件就是,被触发的dom需要设置为position:relative或者position:absolute,否则会返回相对html文档区域左上角的距离
兼容性:IE6/7/8不支持。IE9即以上浏览器解析跟Firefox是一样的。
许多面向对象语言都支持两个种继承:
由于函数没有签名,所以在ECMAScript中无法实现接口继承。所以在ECMAScript只支持实现继承。
原型链:利用原型让一个引用类型继承另一个引用类型的属性和方法。
现在来理清原型,构造函数,实例这三者之间的关系:
现在想一想:如果让原型对象等于另一个类型的实例,结果会怎样?
我们再想一想:假如另一个原型又是另一个类型的实例。按照上述的关系,层层递进。就构造实例与原型的链条,这就是原型链的基本概念。
//定义一个SuperType的构造函数
function SuperType(){
this.property = true;
}
//对SuperType的构造函数的原型添加方法
SuperType.prototype.getSuperValue = function(){
return this.property;
};
//定义另一个构造函数SubType
function SubType(){
this.subproperty = false;
}
//构造函数SubType的原型成为SuperType的实例。
SubType.prototype = new SuperType();
//构造函数SubType的原型添加方法
SubType.prototype.getSubValue = function(){
return this.subproperty;
};
//构造函数SubType的实例
var instance = new SubType();
console.log(instance.property);
我么知道,所有的默认原型都是Object的实例,而这个继承也是通过原型链实现的。
这个要记住:所有的函数的默认原型都是Object实例。因此默认原型都会包含一个内部指针,指向Object.prototype。--------这就是所有的自定义类型都会继承toString(), valueof()等默认方法的根本原因。所以上面的图更完整的是:
第一种:使用instanceof 操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数。结果都会返回true。
//还是上面的那个例子
console.log(instance instanceof Object ); //true
console.log(instance instanceof SuperType); //true;
console.log(instance instanceof SubType); //true;
第二种,使用isPrototypeOf(),同样只要在原型链中出现的原型,都可以说是该原型链所派生的实例的原型。所以该方法也会有返回true.。
//还是上面的的那个例子
console.log(Object.prototype.isPrototypeOf(instance) ); //true
console.log(SuperType.prototype.isPrototypeOf(instance) ); //true;
console.log(SubType.prototype.isPrototypeOf(instance) ); //true;
子类型有时候需要重写超类型的某个方法,或者添加超类型中不存在的某个方法。
但是不管怎样,给原型添加的方法一定要放在替换原型语句的后面
//定义一个SuperType的构造函数
function SuperType(){
this.property = true;
}
//对SuperType的构造函数的原型添加方法
SuperType.prototype.getSuperValue = function(){
return this.property;
};
//定义另一个构造函数SubType
function SubType(){
this.subproperty = false;
}
//构造函数SubType的原型成为SuperType的实例。
SubType.prototype = new SuperType();
//添加新方法
SubType.prototype.getSubValue = function(){
return false;
};
//重写写超类型的方法
SubType.prototype.getSuperValue = function(){
return false;
};
var instance = new SubType();
console.log(instance.getSuperValue()); //false
重写超类型的原型的getSuperValue方法是屏蔽了,超类型的那个同名方法。这这里需要注意的就是
必须使用SuperType的实例替换原型之后,再定义这个方法。
使用原型链时,不要用对象字面量创建原型。因为这样会重写原型链
//定义一个SuperType的构造函数
function SuperType(){
this.property = true;
}
//对SuperType的构造函数的原型添加方法
SuperType.prototype.getSuperValue = function(){
return this.property;
};
//定义另一个构造函数SubType
function SubType(){
this.subproperty = false;
}
//构造函数SubType的原型成为SuperType的实例。
SubType.prototype = new SuperType();
//使用字面量添加新方法,会导致上一句代码失效
SubType.prototype = {
getSubValue : function(){
return this.subPrototype;
}
someOtherMethod : function() {
return false;
}
};
var instance = new SubType();
console.log(instance.getSuperValue) //error;
我们对这段代码分析一下,为什么会出错:
第一: 最主要的问题是在于包含引用类型值的原型会被所有实例共享。也是为什么在构造函数中定义属性,而不在原型中定义属性。(类似原型模式)。
//定义一个构造函数
function SuperType(){
var colors = ["red", "blue"];
}
//定义另一个构造函数
function SubType(){
}
//原型的继承
SubType.prototype = new SuperType();
//SubType构造函数实例化
var instance1 = new SubType();
//这个是SubType()的原型继承与SuperType函数的属性。这个属性在原型中
instance1.colors.push("yellow");
console.log(person1.colors);
//返回red blue yellow
//实例化,继承原型中的属性
var instance2 = new SubType();
console.log(instance2.colors);
//返回red blue yellow
第二:在创建子类型不能向超类型传递参数,实际上。应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。所以一般很少单独使用原型类。
这种方式称为:借用构造函数的技术也称伪造对象或经典继承。
其原理是:在子类型的构造函数调用超类型构造函数。
注意: 函数只不过是在特定环境中执行代码的对象。我们也可用apply()和call()在将来或新创建的对象上执行构造函数。
//定义一个构造函数
function SuperType(){
this.colors = ["red", "blue"];
}
//定义另一个构造函数
function SubType() {
//调用另一个构造函数
SuperType.call(this);
}
//实例化
var instance1 = new SubType();
instance1.colors.push("yellow");
console.log(instance1.colors);
//返回的是red blue yellow
var instance2 = new SubType();
console.log(instance2.colors);
//返回的是red blue
我们对上面这段代码进行分析一下:
传递参数:可以在子类型构造函数中向超类型构造函数传递参数。
//创建一个构造函数
function SuperType(name){
this.name = name;
}
//再创建一个构造函数
function SubType(){
//继承SuperType,传递了参数
SuperType.call(this, "xiaoming");
//实例属性
this.age = 20;
}
//实例话
var instance1 = new SubType();
console.log(instance1.name);
//xiaoming
console.log(instance1.age);
//20
组合继承也称伪经典继承,指的是将原型链与借用构造函数的技术组合在一起,从而发挥二者之长的一种继承模式。其思路是:使用原型链实现对原型属性和方法的继承,而借用构造函数来实现对实例属性的继承。
//定义了一个构造函数
function SuperType(name){
this.name = name;
this.colors = ["red", "blue"];
}
//构造函数的原型
SuperType.prototype.sayName = function() {
console.log(this.name);
};
//另一个构造函数
function SubType(name, age) {
//继承属性
SuperType.call(this, name);
this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
};
//实例化
var instance1 = new SubType("xiaohua", 29);
instance1.colors.push("black");
console.log(instance1.colors);
//返回的是"red,blue,black"
instance1.sayName();
//返回xiaohua
instance1.sayAge();
//返回的是29
var instance2 = new SubType("xiaohua", 29);
console.log(instance2.colors);
//返回的是"red,blue"
instance2.sayName();
//返回xiaohua
instance2.sayAge();
//返回的是29
在这个例子中,我们分析一下:
组合模式JavaScript中最常用的继承方式。
这种继承方式,并没有使用严格意义上的构造函数。其思路是借助原型可以基于自己已有的对象创建新对象,同时还不必因此创建自定义类型。
//定义了一个函数
function object(o){
//创建了一个构造函数
function F(){};
//构造函数的原型赋值为参数o
F.prototype = o;
//返回构造函数新实例
return new F();
}
我们看一个例子:
//引用上面那个方法
var person = {
name :"nike",
friends : ["shelby", "van"]
};
var person1 = object(person);
person1.name = "gery";
person1.friends.push("rob");
var person2 = object(person);
person2,name = "lida";
person2.friends.push("blair");
console.log(person.friends);
//返回shelby, van, rob, blair
对于原型式继承,要求你必须以一个对象作为另一个对象的基础,如果有这么一个对象的话,可以把它传递给object()函数,然后在根据具体需求对得到的对象加以修改即可。
ECMAScript新增了Object.create方法规范化了原型式继承,这个方法接受两个参数,一个是作为新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Obiect.create()和object()行为相同。
var person = {
name :"nike",
friends : ["shelby", "van"]
};
var person1 = Object.create(person);
person1.name = "gery";
person1.friends.push("rob");
var person2 = Object.create(person);
person2,name = "lida";
person2.friends.push("blair");
console.log(person.friends);
//返回shelby, van, rob, blair
该方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同,每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上面的同名属性。
var person = {
name :"nike",
friends : ["shelby", "van"]
};
var person1 = Object.create(person, {
name : {
value : "gery";
}
});
console.log(person1.name)
//返回时gery;
在没有必要的创建构造函数的情况下,而只是想让一个对象与另一个对象保持类似的情况下,原型式继承是不错的选择,但是对于包含引用类型的值的属性始终都会共享相应的值。就像原型模式一样。
寄生式继承的思路是:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
//object就是前面的那个原型式继承的函数
function createAnother(original){
var clone = object(original)
clone.sayHi = function(){
console.log("hi");
}
return clone;
}
var person = {
name : "jerry",
friend : ["shelby", "van"]
};
var person1 =createAnother(person);
person1.sayHi();
//返回的是hi
新对象person1不仅具有person的所有属性和方法,而且还有自己的sayHi()方法。
在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式,object()函数不是必需的,任何能够返回新对象的函数都适用此模式。
使用寄生式继承为对象添加函数,会由于不能做到函数的复用而降低效率。
在前面所说的组合式继承最大的问题是在于:无论在任何情况下,都会调用两次超类型构造函数。分别是在创建子类型原型的时候,一次是在子类型构造函数的内部。最终子类型会包含超类型的全部实例属性,但是我们却不得不在调用子类型构造函数时,重新这些属性。
下面我们看看寄生组合式寄生的例子
function inheritPrototype(subType, superType){
var prototype = Object(superType.prototype); //创建对象
prototype.constructor = subType; //增强对象
subType.ptoyotype = prototype; //指定对象
}
function SubType() {
this.name = name;
this.colors = ["red", "blue"];
}
SuperType.prototype.sayName =function() {
console.name;
};
function SubType(name, age) {
SuperType.call(this, name);
this,age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
console.log(this.age);
}
寄生组合式继承,即通过借用构造函数继承属性,原型链的混成形式来继承方法。其实现方法就是,不必为指定子类型的原型而调用超类型的构造函数,只需要用的是超类型原型的一个副本而已。
function inheritPrototype(subType, superType){
var prototype = Object(superType.prototype); //创建对象
prototype.constructor = subType; //增强对象
subType.ptoyotype = prototype; //指定对象
}
对这个函数进行分析一下
“类”与“行为委托”在理论和思维模型方面都是有区别的,现在看看在真实的场景中如何应用这些方法。
例如在Web开发中非常典型的一种前端场景,创建UI控件(按钮、下拉列表等等)。
例如,例如jQuery来创建一个包含所有通用控件行为的父类(可能叫作Widget)和继承父类的特殊控件子类(比如Button)。
function Widget(width, height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
}
Widget.prototype.render = function($where) {
if(this.$elem) {
this.$elem.css({
width: this.width + 'px',
height: this.height + 'px'
}).appendTo($where);
}
};
//子类
function Button(width, height, lable) {
//调用'super'构造函数
Widget.call(this, width, height );
this.lable = lable || "default";
this.$elem = $('<button>').text(this.lable);
}
//让Button'继承'Widget
Button.prototype = Object.create(Widget.prototype);
//重写render(..)
Button.prototype.render = function($where) {
//'super'调用
Widget.prototype.render.call(this, $where);
//这个父元素点击执行的onClick事件,且作用域绑定在this中
this.$elem.click(this.onClick.bind(this));
};
Button.prototype.onClick = function(evt) {
console.log('Button' + this.lable + 'cliked');
};
$(document).ready(function() {
var $body = $(document.body);
var btn1 = new Button(125, 30, 'hello');
var btn2 = new Button(150, 40, 'world');
btn1.render($body);
btn2.render($body);
});
从上面的代码中,我们出现了显式伪多态,即通过Widget.call和Widget.prototype.render.call从“子类”方法中引用“父类”中的基础方法。
下面是使用class来实现相同的功能
class Widget {
constructor(width, height) {
this.width = width || 50;
this.height= height || 50;
this.$elem = null ;
}
render($where){
if(this.$elem) {
this.$elem.css({
width: this.width + 'px',
height: this.height + 'px'
}).appendTo($where);
}
}
}
class Button extends Widget {
constructor(width, height, label) {
super(width, height);
this.label = label || 'default';
this.$elem = $('<button>').text(this.label);
}
render($where) {
super.render($where);
this.$elem.click(this.onClick.bind(this));
}
onClick(evt) {
console.log('Button "' + this.label + '" cliked');
}
}
$(document).ready(function() {
var $body = $(document.body);
var btn1 = new Button(125, 30, 'hello');
var btn2 = new Button(150, 40, 'world');
btn1.render($body);
btn2.render($body);
});
尽管语法上得到了改进,但实际上这里并没有真正的类,class仍然是通过[[prototype]]机制实现的。
下面使用对象关联风格委托来简单地实现Widget/Button
var Widget = {
init: function(width, height) {
this.width = width || 50;
this.height= height || 50;
this.$elem = null ;
},
insert: function($where) {
if(this.$elem) {
this.$elem.css({
width: this.width + 'px',
height: this.height + 'px'
}).appendTo($where);
}
}
};
var Button = Object.create(Widget);
Button.setup = function(width, height, label) {
//委托调用
this.init(width, height);
this.label = label || 'Default';
this.$elem = $('<button>').text(this.label);
}
Button.build = function($where) {
//委托调用
this.insert($where);
this.$elem.click(this.onClick.bind(this));
};
Button.onClick = function(evt) {
console.log('Button "' + this.label + '" cliked');
}
$(document).ready(function() {
var $body = $(document.body);
var btn1 = Object.create(Button);
btn1.setup(125, 30, 'hello');
var btn2 = Object.create(Button);
btn2.setup(150, 40, 'world');
btn1.build($body);
btn2.build($body);
});
使用对象关联风格编写代码时,不需要把Widget和Button当做父类和子类。相反,Widget只是一个对象,包含一组通用的函数,任何类型的控件都可以委托,Button同样只是一个对。
在委托设计模式下,我们并没有像类一样在两个对象中都定义相同的方法名rrender(..),相反,我们定义了两个更具描述性的方法名(inset(..)和build(..))同理,初始化方法分别叫做init(..)和setup(..).
仔细对比上面代码,你会发现之前调用var btn1 = new Button(..)
现在变成了两次var btn1 = Object.create(Button)
和btn1.setup(..)
。感觉似乎这是一个缺点,但是这一点也是对象关联风格代码相比传统原型风格代码有优势的地方。为什么?
这个场景是这样的,我们有两个控制器对象,一个用来操作网页中的登录表单,另一个用来与服务器进行验证。
在传统的类设计模式,我们会把基础的函数定义名为Controller的类中,然后派生两个子类loginController和AuthController。他们都继承自Controller并且重写了一些基础行为。
//父类
function Controller() {
this.errors = [];
}
Controller.prototype.showDialog(title, msg) {
//给用户显示标题和消息
}
Controller.prototype.success = function(msg) {
this.showDialog('Success', msg);
}
Controller.prototype.failure= function(err) {
this.errors.push(err);
this.showDialog('Error', err);
}
// 子类
function LoginController() {
Controller.call( this );
}
// 将子类链接到父类
LoginController.prototype = Object.create( Controller.prototype );
LoginController.prototype.getUser = function() {
return document.getElementById( "login_username" ).value;
};
LoginController.prototype.getPassword = function() {
return document.getElementById( "login_password" ).value;
};
LoginController.prototype.validateEntry = function(user,pw) {
user = user || this.getUser();
pw = pw || this.getPassword();
if (!(user && pw)) {
return this.failure( "Please enter a username & password!" );
}
else if (pw.length < 5) {
return this.failure( "Password must be 5+ characters!" );
}
// 到这里了?输入合法!
return true;
};
// 覆盖来扩展基本的`failure()`
LoginController.prototype.failure = function(err) {
// "super"调用
Controller.prototype.failure.call( this, "Login invalid: " + err );
};
// 子类
function AuthController(login) {
Controller.call( this );
// 除了继承外,我们还需要合成
this.login = login;
}
// 将子类链接到父类
AuthController.prototype = Object.create( Controller.prototype );
AuthController.prototype.server = function(url,data) {
return $.ajax( {
url: url,
data: data
} );
};
AuthController.prototype.checkAuth = function() {
var user = this.login.getUser();
var pw = this.login.getPassword();
if (this.login.validateEntry( user, pw )) {
this.server( "/check-auth",{
user: user,
pw: pw
} )
.then( this.success.bind( this ) )
.fail( this.failure.bind( this ) );
}
};
// 覆盖以扩展基本的`success()`
AuthController.prototype.success = function() {
// "super"调用
Controller.prototype.success.call( this, "Authenticated!" );
};
// 覆盖以扩展基本的`failure()`
AuthController.prototype.failure = function(err) {
// "super"调用
Controller.prototype.failure.call( this, "Auth Failed: " + err );
};
var auth = new AuthController(
// 除了继承,我们还需要合成
new LoginController()
);
auth.checkAuth();
所有控制器共享的基础行为是success(..)和failure(..)和showDialog(..).子类LoginController和AuthController通过重写success(..)和failure(..)来扩展默认基础类的行为。此外,注意AuthController需要LoginController的实例来和登陆表单进行交互,因此这个实例变成了数据属性。
你可能想让LoginController继承AuthController或者相反,这样我们就通过继承链实现真正的合成。但是这就是类继承在问题领域建模时会产生的问题,因为LoginController和AuthController都不具备对方的基础行为,所以这种继承关系是不恰当的。解决方法是进行一些简单的合成从而让他们既不必互相继承又可以互相合作。
但是,我们真的需要用一个父类,两个子类加上合成来对这个问题建立模型吗?能不能用对象关联风格的行为委托来实现更简单的设计。当然是可以的!
var LoginController = {
errors: [],
getUser: function() {
return document.getElementById( "login_username" ).value;
},
getPassword: function() {
return document.getElementById( "login_password" ).value;
},
validateEntry: function(user,pw) {
user = user || this.getUser();
pw = pw || this.getPassword();
if (!(user && pw)) {
return this.failure( "Please enter a username & password!" );
}
else if (pw.length < 5) {
return this.failure( "Password must be 5+ characters!" );
}
// 到这里了?输入合法!
return true;
},
showDialog: function(title,msg) {
// 在对话框中向用于展示成功消息
},
failure: function(err) {
this.errors.push( err );
this.showDialog( "Error", "Login invalid: " + err );
}
};
// 链接`AuthController`委托到`LoginController`
var AuthController = Object.create( LoginController );
AuthController.errors = [];
AuthController.checkAuth = function() {
var user = this.getUser();
var pw = this.getPassword();
if (this.validateEntry( user, pw )) {
this.server( "/check-auth",{
user: user,
pw: pw
} )
.then( this.accepted.bind( this ) )
.fail( this.rejected.bind( this ) );
}
};
AuthController.server = function(url,data) {
return $.ajax( {
url: url,
data: data
} );
};
AuthController.accepted = function() {
this.showDialog( "Success", "Authenticated!" )
};
AuthController.rejected = function(err) {
this.failure( "Auth Failed: " + err );
};
由于AuthController只是一个对象(LoginController也是一样),因此我们不需要实例化(new AuthController() ),只需要AuthController.checkAuth()
借助对象关联,你就可以简单地向委托链添加一个或多个对象,而且同样不需要实例化。
var controller1 = Object.create(AuthController);
var controller2 = Object.create(AuthController);
在行为委托模式中,AuthController和LoginController只是对象,他们之间的关系只是兄弟关系,并不是父类和子类的关系。代码中AuthController和LoginController。反向委托也完全没问题的。
在这种模式的重点在于只需要两个实体(AuthController和LoginController),而之前的模式需要三个。
我们不需要Controller基础类“共享‘两个实体之间的行为,因为委托足以满足我们需要的功能,同样,我们也不需要实例化类,因为他们根本就不是类,他们只是对象,此外,我们也不需要合成,因为另个对象可以通过委托进行合作。
最后,我们避免了面向类设计模式的多态,我们在不同的对象中没有使用相同的函数名success(..)和failure(..),这样就不需要使用显示伪多态。相反,在AuthController中他们的名字accepted(..)和rejected(..)------可以更好地描述他们的行为。
javascript中万物皆为对象这是一种常见的错误说法。
对象是javascript的基础。在javascript中一共有六种主要类型。
注意,简单基本类型(string, number,boolean, null,undefined)本身并不是对象。
null有时会被当作一种对象类型,但是这其实只是语言的一个bug,即对null执行typeof null时会返回字符串
’object‘,实际上,null本身是基本类型。
不同的对象在底层都表示二进制,在javascript中的二进制前三位都为0的话会被判断为object类型,null的二进制表示全为0,自然前三位也是0,所以typeof时会返回’object‘.
实际上,javascript中有许多特殊的对象子类型,我们可以称之为复杂基本类型。
函数就是对象的一个子类型。javascript中的函数本质上和普通的对象一样(只是可以调用),所以可以像操作其他对象一样操作函数(比如当做另一个函数的参数)。
数组也是对象的一种类型,具备一些额外的行为。数组中的内容的组织方式比一般的对象稍微复杂以一些。
javascript中还有一些对象的子类型,通常被称为内置对象。有些内置对象的名字看起来和简单基础类型一样,不过实际上他们的关系更复杂。
在javascript中,这些内置对象实际上只是一些内置函数。这些内置函数可以当作构造函数。从而可以构造一个对应子类型的新对象。
// 简单基础类型
var strPrimitive = 'i am a string';
typeof strPrimitive ; // 'string'
//是否为对象的子类型String
strPrimitive instanceof String; //false
var strObject = new String('i am a string');
typeof strObject ; //object;
strObject instanceof String; //true
原始值'i am a string'并不是一个对象,它只是一个字面量,并且是一个不可变的值。如果要在这个字面量上执行一些操作。比如获取长度。那就需要将其转换为String对象。
在必要时语言会自动把字符串字面量转换成一个String对象,也就是说你并不需要显示创建一个对象。
var strPrimitive = 'i am a string';
console.log(strPrimitive .length); //13
console.log(strPrimitive.charAt(3)); // 'm'
使用以上两种方法,我们都可以直接在字符串字面量上访问和属性和方法,之所以可以这样做,是因为引擎自动把子面量转换成String对象。所以可以访问属性和方法。
同样的事也会发生在数值字面量上,如果使用类型43.232.toFixed(2)的方法,引擎会把42转换成new Number(42)。对于布尔字面量也是如此
null和undefined没有对应的构造函数。他们只有文字形式。
Date只有构造函数,没有文字形式。
对于Object, Array, Function, RegExp来说,无论使用文字形式还是构造形式,他们都是对象,不是字面量。
Error对象很少在代码中显示创建,一般是在抛出异常时,自动创建。也可以使用new Error(..)这种构造形式来创建。
对象的内容是由一些存储在特定命名位置的(任意类型的)值组成的,我们称之为属性。
当我们在说’内容‘时,似乎在暗示这些值实际上被存储在对象内容,但是这只是它的表现显示。在引擎内部,这些值的存储方式是多种多样的,一般并不会存在对象内部。存储在对象容器内部的是这些属性的名称,他们就像指针一样,指向这些真正的存储位置。
var myObject = {
a: 2
}
myObject.a; //2
myObject[a]; //2
访问myObject中a位置上的值,我们可以使用
.
称为属性访问这两种语法主要区别,操作符要求属性名满足标识符的命名规范,而["..."]语法可以接受任意的UTF-8/Unicode字符串作为属性名。
在对象中,属性名永远都是字符串。如果你使用string(字面量)以外的其他值作为属性名,那它首先会被转换为一个字符串。即使是数字也不例外。虽然在数组下标中使用的的确是数字,但是在对象属性名中的数字会被转换成字符串。所以要当心不要搞混对象和数组中数字的用法。
var myObject = { };
myObject[true] = 'foo';
myObject["true"] = 'foo';
每次访问对象的属性就是属性的访问,如果属性访问返回的是一个函数,那它也并不是一个“方法”。属性访问返回的函数和其他函数没有任何区别(除了可能发生的隐式绑定this)
function foo() {
console.log('foo');
}
var someFoo = foo; //对foo的变量引用
var myObject = {
someFoo: foo
}
console.log(foo);
console.log(someFoo);
console.log(myObject.someFoo);
//三者的结果是一样的
someFoo和myObject.someFoo只是对同一个函数的不同引用,并不能说明这个函数是特别的或者’属于‘某个对象,如果foo()定义时在内部有一个this引用,那这两个函数引用的唯一区别就是myObjet.someFoo中的this会被隐式绑定都一个对象。无论那种引用形式都不能称之为’方法‘。
即使你在对象的文字形式中声明一个函数表达式,这个函数也不会’属于‘这个对象----他们只是对于相同函数对象的多个引用。
var myObject = {
foo: function(){
console.log("foo");
}
};
var someFoo = myObject.foo;
console.log(someFoo);
console.log(myObject.foo);
//两着的情况是一样的
数组也是对象,虽然每个下标都是整数,但是任然可以给数组添加属性。
var myArray = ['foo', 42, 'bar'];
myArray.baz = 'baz';
console.log(myArray.length); //3
console.log(myArray.baz); //'baz'
可以看到虽然添加了命名属性(无论是通过,语法还是[]语法),数组的length值并未发生过变化。
你可以把数组当成一个普通的键/值的对象来使用,并且不添加任何数组索引,但是这并不是一个好主意,数组和普通的对象都根据其对应的行为和用途进行了优化,所以最好只用对象来存储键/值对,只用数组来存储数值下标/值对。
注意:如果你试图向数组添加一个属性,但属性名’看起来‘像一个数字,那它会变成一个数值下标(因此会修改数组内容而不是添加一个属性)
var myArray = ['foo', 42, 'bar'];
myArray['3'] = 'baz';
console.log(myArray.length); //4
console.log(myArray[3]); //'baz'
看下面的一个例子,思考如何准确表示的muObject的复制呢?
function anotherFunction(){}
var anotherObject = {
c: true
};
var anotherArray = [];
var myObject = {
a: 2,
b: anotherObject, //引用,不是副本
c: anotherArray, //另一个引用
d: anotherFunction
};
anotherArray.push(anotherObject, myObject);
首先,我们应该考虑他是浅复制还是深复制
对于浅复制来说,复制出的对象中的a的值会复制旧对象中的a值,也就是2。但是新对象中的b、c、d三个属性其实只是引用,他们和旧对象中b、c、d引用的对象是一样。
对于深复制,除了复制myObject以外的还会复制anotherObject和anotherArray。这样问题就出现了,anotherArray引用了anotherObject和myObject,所以又要复制myObject。这样就会由于循环引用导致死循环。
相对与深复制,浅复制就比较简单了。在ES6定义了Object.assign(..)方法来实现浅复制。该方法的第一个参数是目标对象,之后还有可以跟一个或多个源对象。他会遍历一个或者多个源对象的所有可枚举的自有键并把他们复制(使用=操作符赋值)到目标对象。最后返回目标对象。
var newObj = Object.assign({}, myobject);
console.log(newObj.a); //2
console.log(newObj.b === anotherObject); //true
由于Object.assign(...)就是使用 = 操作符来赋值,所以源对象属性的一些特性(例如:writable)不会被复制到目标对象。
var myObject = {
a: 2
};
Object.getOwnPropertyDescriptor(myObject, 'a');
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
这个普通的对象属性对应的属性描述符不仅仅只是一个2,它还包含另外三个特性:writable(可写),enumerable(可枚举)和configurable(可配置)。
在创建普通属性时属性描述符会使用默认值,我们可以使用Object.defineProperty(..)来添加一个新属性或者修改一个已有的属性(如果它是configurable)并对特性进行设置
var myObject = {};
Object.defineProperty(myObject, 'a', {
value: 2,
writable: true,
enumerable: true,
configurable: true
} );
console.log(myObject.a); //2
这几个属性可参见前面的几篇博文。
有时候,你希望属性或者对象是不可改变的。在ES5中可以通过很多中方法来实现。
myxxObject.foo; //[1, 2, 3]
myxxObject.foo.push(4); //[1, 2, 3]
myxxObject.foo; //[1, 2, 3, 4]
假设代码中的myxxObject已经被创建而且是不可变的,但是为了保护它的内容myxxObject.foo,你还需要使用下面的方法让foo也不可变。
结合writable: false 和configurable:false就可以创建一个真正的常量属性(不可修改,重定义或者删除)
var myxxObject = {};
Object.defineProperty(myxxObject, 'foo', {
value = 4,
writable: false
configurable:false
});
如果你想禁止一个对象添加新属性并且保留已有属性。可以使用Object.preventExtensions(..):
var myxxObject = {
foo: 2
};
Object.preventExtensions(myxxObject );
myxxObject.b =3;
console.log(myxxObject.b); //undefined
在非严格模式下,创建属性b会静默失败,在严格模式下,将会抛出TypeError错误。
Object.seal(..)会创建一个’密封‘的对象,这个方法实际上会在一个现有的对象上调用Object.preventExtensions(..):,并且把所以现有的属性标记为configurable:false
所以,密封后不仅不能添加新属性,也不能重新配置或者删除任何现有的属性(虽然可以修改属性值)
Object.freeze(..)会创建一个冻结对象,这个方法实际上会在现有的对象上调用Object.seal(..)并把所有的’数据访问‘属性标记为writable: false,这样就无法修改他们的值。
var myObject = {
a: 2
}
console.log(myObject.a); //2
思考上面的代码,myObject.a是一次属性访问,但是这条语句并不仅仅是在myObject中查找名字为a的属性,虽然看起来是这样的。
在语言的规范中,myObject.a在myObject上实际上是实现了[[Get]]操作(有点像函数调用[Get])。对象默认内置[[Get]]操作首先在对象中查找是否有名称相同的属性,如果找到就会返回这个属性值。
然而,如果没有找到名称相同的属性,按照[[Get]]算法的定义会遍历可能存在的[[prototype]]链,也就是原型链。
如果无论如何都没找到名称相同的属性,那[[Get]]操作会返回undefined。
var myObject = {
a: 2
}
console.log(myObject.b); //undefined
注意,这种方法和访问变量时是不一样的,如果你引用了一个当前词法作用域中不存在的变量,并不会像对象属性一样返回undefined,而是抛出一个ReferenceError异常。
var myObject = {
a: undefined
}
console.log(myObject.a); //undefined
console.log(myObject.b); //undefined
从上面的代码来看,这两个引用并没有区别,他们都是返回了undefined,实际上底层的[[Get]]操作对myObject.b进行更复杂的处理。
既然有可以获取属性值的[[Get]]操作,就一定有对应的[[Put]操作。可能会认为给对象的属性赋值会触发[[Put]]来设置或者创建这个属性。但是实际情况并不是这样的。
[[Put]]被触发时,实际的行为取决许多的因素,包括对象中是否已经存在这个属性(这个数最重要的因素)-----如果已经存在这个属性,[[Put]]算法大致会检查下面这些内容。
如果对象中不存在这个属性,[[Put]]操作会更加复杂。
对象默认的[[Put]]和[[Get]]操作分别可以控制属性值的设置和获取。
在ES5中可以使用getter和setter部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上,getter是一个隐藏函数,会在获取属性值时调用,setter也是隐藏函数,会在设置属性值调用。
当你可以属性定义getter、setter或者两者都有时,这个属性会被定义为‘访问描述符’(和数据描述符相对)。对于访问描述符来说,javascript会忽略他们的value和writable特性,取而代之是关心set和get(还有configurable和enumerable)特性。
var myObject = {
//给a定义一个getter
get a() {
return: 2;
}
};
Object.defineProperty(
myObject, //目标对象
‘b’, //属性名
{
//描述符
//给b设置一个getter
get: function(){
return: this.a*2;
}
//确保b会出现在对象的属性列表中
enumerable: true
}
);
myObject.a; //2
myObject.b; //4
不管是对象文字语法中的get a() {...},还是defineProperty(..)中显示定义,二者都会在对象中创建一个不包含值的属性,对于这个属性的访问会自动调用一个隐藏函数,他的返回值会被当做属性访问的返回值。
var myObject = {
//给a定义一个getter
get a() {
return: 2;
}
};
myObject.a = 3;
console.log(myObject.a); //2
由于我们只定义了a的setter,所以对a的值进行设置时set操作会忽略赋值操作,不会抛出错误。而且即便有合法的setter,由于我们自定义的getter只会返回2,所以set操作是没有意义的。
为了让属性更合理,还应当定义setter,和你期望的一样的,setter会覆盖的那个属性的默认的[Put]操作,通常来说getter和setter是成对出现的。(只定义一个的话,通常会产生意料之外的行为):
var myObject = {
//给a定义一个getter
get a() {
return: this._a_;
},
set a(val) {
this._a_ = val*2;
}
};
myObject.a = 2;
console.log(myObject.a); //4
在这个例子中,我们赋值[[Put]]操作中的值2存储到了另一个变量_a_中,名称的_a_只是一种惯例,没有任何特殊的行为----和其他普通属性一样。
在数组上应用for...in循环有时会产生出人意料的结果,因为这种枚举不仅会包含所有的数值索引,还会包含所有可枚举的属性。最好只在对象上应用for...in循环,如果要遍历数组就使用传统的for循环来遍历数值索引。
propertyIsEnumerable(..)会检查给定的属性名是否直接存在与对象中(而不是在原型链上)并且满足enumerable:true.
如何直接遍历值而不是数组的下标(或者对象的属性)呢?ES6新增的一种用来遍历数组的for..of循环语法(如果对象本身定义了迭代器的话,也可以遍历对象):
var myArray = [1, 2, 3];
for(var v of myArray) {
console.log(v);
}
//1 2 3
for..of循环首先会想被访问对象请求一个迭代器对象,然后通过迭代器对象的next()方法来遍历所有的返回值。
在开发中经常需要知道某一个节点是不是另一个节点的后代,IE为此率先引入了该方法,以便不通过在DOM文档树中查找即可获得这个信息。
调用contains()方法的应该是祖先节点,也就是搜索开始的节点,这个方法接受一个参数,即要检测的后代节点,如果被检测的节点是后代节点,该方法返回true,否则,返回false。
alert(document.documentElement.contains(document.body)); //true
上面测试了body元素是否为html元素的后代。支持contains方法的浏览器有IE firefox9+ Safiri Opera9.5+和chrome。
使用DOM Level3 compareDocumentPosition()也能够确定节点间的关系。支持这个方法的浏览器
IE9+ firefox Safiri Opera9.5+和chrome。
这个方法用于确定两个节点间的关系,返回一个表示关系的位掩码。下面是各个位掩码所代表的值。
掩码 | 节点关系 |
---|---|
1 | 无关(给定节点不在当前文档中) |
2 | 居前(给定的节点在DOM树中位于参考及节点之前) |
4 | 居后 (给定的节点在DOM树中位于参考及节点之后) |
8 | 包含(给定的节点是参考节点的祖先) |
16 | 被包含 (给定的节点是参考节点的后代) |
32 | 没有关系,或是两个节点是同一元素的两个属性 |
var result = document.documentElement.compareDocumentPosition(document.body);
alert(!!(result & 16));
执行上面的代码后,结果会变成20(表示‘居后’的4加上表示‘被包含’的16)。对位掩码16执行按位操作会返回一个非零的数值,而两个逻辑非操作符会将该数值转换成布尔值。
兼容的contains函数
function contains(refNode, otherNode) {
if(typeof refNode.contains == 'function' && (!client.engine.webkit || client.engine.webkit >= 522)){
return refNode.contains(otherNode);
}else if(typeof refNode.compareDocumentPosition == 'function' ) {
return !!(refNode.compareDocumentPosition(otherNode) & 16);
}else {
var node = otherNode.parentNode;
do{
if(node === refNode){
return true
}else{
node = node.parentNode;
}
}while(node !==null);
return false;
}
}
!!一般用来将后面的表达式强制转换为布尔类型的数据(boolean),也就是只能是true或者false;
var a;
var b=!!a;
a默认是undefined。!a是true,!!a则是false,所以b的值是false,而不再是undefined,也非其它值,主要是为后续判断提供便利。
!!一般用来将后面的表达式强制转换为布尔类型的数据(boolean),也就是只能是true或者false;
因为javascript是弱类型的语言(变量没有固定的数据类型)所以有时需要强制转换为相应的类型,
《javascript高级程序设计》
javascript contains方法
http://www.cnblogs.com/lhb25/archive/2011/09/06/javascript-scope-chain.html
http://www.laruence.com/2009/05/28/863.html
https://www.zhihu.com/question/36393048
读了上面三篇文章,对一直很困惑的作用域有了进一步的了解。真的很感谢博主们用心的写的博文和知友的回答。现在把里面能理解到相关知识都整合一起,方便以后理解。
<script>
//并不会弹出function的。因为这是两个script标签。
alert(typeof eve); //结果:undefined
</script>
<script>
function eve() {
alert('I am Laruence');
}
</script>
JS在执行每一段JS代码之前, 都会首先处理var关键字和function定义式(函数定义式和函数表达式).
在调用函数执行之前, 会首先创建一个变量对象, 然后搜寻这个函数中的局部变量定义,和函数定义, 将变量名和函数名都做为这个变量对象的同名属性, 对于局部变量定义,变量的值会在真正执行的时候才计算, 此时只是简单的赋为undefined.
alert(typeof eve); //结果:function
alert(typeof walle); //结果:undefined
function eve() { //函数定义式
alert('I am Laruence');
};
var walle = function() { //函数表达式
}
alert(typeof walle); //结果:function
这就是函数定义式和函数表达式的不同, 对于函数定义式, 会将函数定义提前. 而函数表达式, 会在执行过程中才计算.(这就是说函数的声明的定义函数的方式,有一个重要的特征就是函数声明提升,其意思是在执行代码之前,就会读取函数的声明)
注意到, 因为函数对象的[[scope]]属性是在定义一个函数的时候决定的, 而非调用的时候。
function func(lps, rps){
var name = 'laruence';
}
此时我们只有一个执行环境那就是全局执行环境。里面有一个func对象,其值是未定义的。func()函数未调用之前会创建一个执行环境-------而每个执行环境都有与之关联的变量对象(变量对象就是执行环境中包含了所有变量和函数的对象。另外变量对象是后台的,保存在内存中的,代码无法直接访问的)----------这个变量对象的有一个特殊的内部属性[scope]--------这个属性就是的就是作用域链的值。(这些关系都是静态的话,还没有开始执行,这只是这个函数拥有的关系)。
function func(lps, rps){
var name = 'laruence';
}
func();
我们调用func()函数----------会创建一个执行环境-------而每个执行环境都有与之关联的活动对象(变量对象就是执行环境中包含了所有变量和函数的对象。另外活动对象是后台的,保存在内存中的,代码无法直接访问的)----------这个活动对象的有一个特殊的内部属性[scope]--------这个属性就是
的就是作用域链的值。此时全部执行环境的活动变量处于第二位,当前的执行环境的活动对象处于第一位。
在发生标识符解析的时候, 就会逆向查询当前scope chain列表的每一个活动对象的属性,如果找到同名的就返回。找不到,那就是这个标识符没有被定义。
function returnfunc (propertyName) {
return function (obj) {
return obj[propertyName];
};
}
var savefunc = returnfunc("name");
var result = savefunc({name:"Picasso"});
alert(result);
以上代码的最开始的作用域链和执行环境:
我们先开始调用returnfunc()函数,马上会创建一个包含returnfunc()变量对象的行环境,作用域链开始变化,如下图:
随后returnfunc()函数会返回它内部的匿名函数,当匿名函数被返回后,整个作用域链和执行环境又发生了变化:
我们看到匿名函数(闭包)被添加到了最作用域链的最前端,returnfunc()的执行环境被销毁,但我们注意到returnfunc()函数的活动对象仍然在被引用(匿名函数仍在访问propertyName参数),因此returnfunc()函数的变量对象仍然在内存中,成为活动对象。这就是为什么匿名函数就能访问returnfunc()函数定义的所有变量和全局环境定义的变量,毕竟returnfunc()的活动对象仍然保持“激活”状态。
知道匿名函数被销毁,returnfunc()函数活动对象才被销毁
var name = 'laruence';
function echo() {
alert(name);
var name = 'eve';
alert(name);
alert(age);
}
echo();
/*
其结果为:
undefined
eve
[脚本出错]
*/
因为js预编译----- 在调用函数执行之前, 会首先创建一个变量对象, 然后搜寻这个函数中的局部变量定义,和函数定义, 将变量名和函数名都做为这个变量对象的同名属性, 对于局部变量定义,变量的值会在真正执行的时候才计算, 此时只是简单的赋为undefined. 这句话很重要,要再提醒一遍。
开始执行echo()函数的时候,js预编译的时候,name设置了undefine,执行的echo()函数的时候,函数内部定义的变量name赋值了,即找到了,所以弹出eve,而age根本就没定义,所以会报错。
OPTIONS:返回服务器针对特定资源所支持的HTTP请求方法。也可以利用向Web服务器发送'*'的请求来测试服务器的功能性。
HEAD:向服务器索要与GET请求相一致的响应,只不过响应体将不会被返回。这一方法可以在不必传输整个响应内容的情况下,就可以获取包含在响应消息头中的元信息。
GET:向特定的资源发出请求。
POST:向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的创建和/或已有资源的修改。
PUT:向指定资源位置上传其最新内容。
DELETE:请求服务器删除Request-URI所标识的资源。
TRACE:回显服务器收到的请求,主要用于测试或诊断。
CONNECT:HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
虽然HTTP的请求方式有8种,但是我们在实际应用中常用的也就是get和post,其他请求方式也都可以通过这两种方式间接的来实现。
ORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
只要同时满足以下两大条件,就属于简单请求。
凡是不同时满足上面两个条件,就属于非简单请求。
浏览器对这两种请求的处理,是不一样的。
对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。
下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。
GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。
(1)Access-Control-Allow-Origin
该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
(2)Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
(3)Access-Control-Expose-Headers
该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。
3.2 withCredentials 属性
上面说到,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。
Access-Control-Allow-Credentials: true
另一方面,开发者必须在AJAX请求中打开withCredentials属性。
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。
但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials。
xhr.withCredentials = false;
需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。
4.1 预检请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。
下面是一段浏览器的JavaScript脚本。
var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
上面代码中,HTTP请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header。
浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的HTTP头信息。
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
"预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。
除了Origin字段,"预检"请求的头信息包括两个特殊字段。
(1)Access-Control-Request-Method
该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。
(2)Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header
4.2 预检请求的回应
服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com
可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。
Access-Control-Allow-Origin: *
如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。
XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
服务器回应的其他CORS相关字段如下
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
(1)Access-Control-Allow-Methods
该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
(2)Access-Control-Allow-Headers
如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
(3)Access-Control-Allow-Credentials
该字段与简单请求时的含义相同。
(4)Access-Control-Max-Age
该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
4.3 浏览器的正常请求和回应
一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。
下面是"预检"请求之后,浏览器的正常CORS请求。
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
上面头信息的Origin字段是浏览器自动添加的。
下面是服务器正常的回应。
Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8
上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的。
在IE7及更高版本,Firefox,opera,Chrome和Safari浏览器中,都支持XHR对象,在这些浏览器中创建XHR对象直接像下面一样使用XMLHttpRequest()构造函数即可。
var xhr = new XMLHttpRequest()
http是计算机通过网络进行通信的规则。浏览器即客户端通过协议向服务器请求信息和服务。HTTP是一种无状态的协议,是指不建立持久的连接。HTTP目前协议的版本是1.1.HTTP是一种无状态的协议,无状态是指Web浏览器和Web服务器之间不需要建立持久的连接,这意味着当一个客户端向服务器端发出请求,然后Web服务器返回响应(response),连接就被关闭了,在服务器端不保留连接的有关信息.HTTP遵循请求(Request)/应答(Response)模型。Web浏览器向Web服务器发送请求,Web服务器处理请求并返回适当的应答。所有HTTP连接都被构造成一套请求和应答。
在使用XHR对象时,要调用的第一个方法是open()方法,它接受3个参数,要发送的请求的类型(“get”,“post”)、要请求的URL、表示是否异步请求的布尔值。
xhr.open("get", "example.php",true)
运行这行代码,会启动一个针对example.php的GET请求。在这里,url相对于执行代码的当前页面(当然也可以使用绝对路径),调用open()方法并不会发送真正的请求。只是启动一个请求以备发送。
如果要发送特定的请求,必须像下面这样调用open()方法,
xhr.open("get", "example.php",false)
xhr.send(null)
这里send()方法接受一个参数,即要作为请求主体发送的数据。如果不需要通过请求主体发送数据,则必须传入null。因为这个参数对有些浏览器来说是必需的,调用send()之后,请求就会被分派到服务器。
在收到响应时,要经历的过程。
下面是一个例子:
xhr.open("get", "example.php",fasle);
xhr.send(null);
if(xhr.status>=200 && xhr.status<300 || xhr.status == 304) {
console.log(xhr.responseText);
}else {
console.log("request was unsuccessful: " + xhr.status);
}
通过检测status来决定下一步操作,无论内容类型是什么,响应主体的内容都会保存在responseText属性中,而对于非XML数据而言,responseXML属性为null。
前面都是同步的请求,但大多数我们发送的是异步的请求。使JavaScript继续执行而不必等待响应。
此时我们可以检测XHR对象的readyState的属性。该属性表示请求/响应过程的当前活动阶段。这个属性可取以下值。
0:未初始化,尚未调用open()方法。
1:启动,已经调用open()方法,但尚未调用send()方法,
2:发送,已经调用send()方法,但尚未收到响应。
3:接收,已经接收到部分响应数据。
4:完成,已经接收到全部响应数据,而且可以在客户端使用了。
只要readyState属性改变,就会触发readystatechange事件。可以利用这个事件来检测每次状态变化后的readyState的值。(注意必须在调用open()之前,指定onreadystatechange事件处理程序才能保证跨浏览器兼容性)。
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if(xhr.readyState == 4) {
if(xhr.status>=200 && xhr.status<300 || xhr.status == 304) {
console.log(xhr.responseText);
}else{
console.log("request was unsuccessful: " + xhr.status);
}
}
};
xhr.open("get", "example",true);
xhr.send(null);
注意在这个例子中,利用的dom0级的事件处理程序,没有使用dom2级是因为并非所有浏览器都支持dom2级,与其他事件处理程序不同。这里并没有传递event对象。而是通过XHR对象本身来确定下一步该怎么做。
在默认情况下,在发送XHR请求同时,还会发送以下头部信息
1.Accept: 浏览器能够处理的内容类型。
2.Accept-Charset: 浏览器能显示的字符集
3.Accept-Encoding:浏览器能够处理的压缩字编码
4.Accept-Language:浏览器当前设置的语言。
5.Connection: 浏览器与服务器建立的连接。
6.Cookie:当前页面设置的任何cookie。
7.Host:发出请求的页面所在域。
8.Referer:发出请求的页面的URL。
可以使用setRequestHeader()方法,设置自定义的请求头部。该方法接受两个参数,一个是头部字段的名称,一个数头部字段的值。要成功发送请求头部信息,必须在调用open()方法之后,且调用send()方法之前调用。
//还是上面的那个例子
xhr.open("get", "example",true);
xhr.setRequestHeader("myheader","myvalue");
xhr.send(null);
getRequestHeader()方法可以取得一个包含所有头部信息的长字符串。
var myheader = xhr.getRequestHeader("myheader");
var allheader = xhr.getRequestHeader();
GET请求时最常见的请求,最常用于向服务器查询某些信息,必要时,可以将查询的字符串参数追加到URL的末尾,以便将信息发送给服务器,对于XHR而言,位于传入open()方法的URL末尾的查询字符串必须经过正确的编码才行。
在使用GET请求经常发生的错误在于,就是查询字符串的格式有问题,查询字符串中的每一个参数名称和值都必须使用encodeURLComponent()进行编码,然后才能放到URL的末尾,而且所有名-值对必须由&分隔,例如:
xhr.open("get", "example.php?name1=value1&name2=value2",value);
可以用以下函数。
function addURLparam(url, name, value){
//查询当前url,如果不存在?那么就加上?,不然加上&
url += (url.indexof("?") == -1) ? "?" : "&";
url += encodeURLComponent(name) + "=" + encodeURLComponent(value);
return url;
}
//利用上面的这个函数
var url = "example.php";
//添加参数
var url = addURLparam(url, "name", "jerry");
url = addURLparam(url, "book", "ps");
//初始化请求
xhr.open("get",url,false);
post请求用于向服务器发送应该被保存的数据,POST请求应该把数据作为请求主体提交。
看一个例子
//前面的例子
xhr.open("post", "example.php", true);
xhr.setRequestHeader("Content-Type","application/x-www-form-urlencode");
xhr.send("name=xiaoxiao");
例如在后台php语言中,对前端请求作出的响应。返回的是一个json对象。
echo '{"success":false,"msg":"参数错误"}';
//或者
$result = '{"success":true,"msg":"找到员工:员工编号:' . $value["number"] . ',员工姓名:' . $value["name"] . ',员工性别:' . $value["sex"] . ',员工职位:' . $value["job"] . '"}';
我们在前端一般这样处理(对于json我们有节总结)
var request = new XMLHttpRequest();
request.open("GET", "serverjson.php?number=" + document.getElementById("keyword").value);
request.send();
request.onreadystatechange = function() {
if (request.readyState===4) {
if (request.status===200) {
//这里我们把后台传过来的json数据,解析为JavaScript对象
var data = JSON.parse(request.responseText);
if (data.success) {
document.getElementById("searchResult").innerHTML = data.msg;
} else {
document.getElementById("searchResult").innerHTML = "出现错误:" + data.msg;
}
} else {
alert("发生错误:" + request.status);
}
}
}
参数分别代表的是:要请求的地址,发送到服务器的一个简单对象(含有零个或多个的key/value对)或者字符串,请求完成的回调函数。
//例1,类似以GET提交
$( "#result" ).load( "ajax/test.html" );
//在这里需要注意,如果文档中没有#result的元素,那么这个请求并不会发送
//例2,回调函数
$( "#result" ).load( "ajax/test.html", function() {
alert( "Load was performed." );
});
//例3.POST提交
$( "#feeds" ).load( "feeds.php", { limit: 25 }, function() {
alert( "The last 25 entries in the feed have been loaded" );
});
//data参数是从服务器传过来的数据。
$.get( "test.php", function( data ) {
$( "body" )
.append( "Name: " + data.name ) // John
.append( "Time: " + data.time ); // 2pm
}, "json" );
$.post( "test.php", { func: "getNameAndTime" }, function( data ) {
console.log( data.name ); // John
console.log( data.time ); // 2pm
}, "json");
这两个参数,一个是请求地址,第二个是选择的参数列表(下面)。
参数名 | 类型 | 描述 |
---|---|---|
url | String | (默认: 当前页地址) 发送请求的地址。 |
type | String | (默认: "GET") 请求方式 ("POST" 或 "GET"), 默认为 "GET"。注意:其它 HTTP 请求方法,如 PUT 和 DELETE 也可以使用,但仅部分浏览器支持。 |
timeout | Number | 设置请求超时时间(毫秒)。此设置将覆盖全局设置。 |
async | Boolean | (默认: true) 默认设置下,所有请求均为异步请求。如果需要发送同步请求,请将此选项设置为 false。注意,同步请求将锁住浏览器,用户其它操作必须等待请求完成才可以执行。 |
beforeSend | Function | 发送请求前可修改 XMLHttpRequest 对象的函数,如添加自定义 HTTP 头。XMLHttpRequest 对象是唯一的参数。function (XMLHttpRequest) { this; // the options for this ajax request } |
cache | Boolean (默认: true) | jQuery 1.2 新功能,设置为 false 将不会从浏览器缓存中加载请求信息。 |
complete | Function | 请求完成后回调函数 (请求成功或失败时均调用)。参数: XMLHttpRequest 对象,成功信息字符串。function (XMLHttpRequest, textStatus) { this; // the options for this ajax request } |
contentType | String | (默认: "application/x-www-form-urlencoded") 发送信息至服务器时内容编码类型。默认值适合大多数应用场合。告诉服务器从浏览器提交过来的数据格式。例如:我们提交数据时假如使用了 JSON2.js 中方法 JSON.stringify(obj) 格式化为json字符串后,再默认提交就会报错。这个时候就需要指定提交的内容格式为:"application/json"。data Object,String 发送到服务器的数据。若data数据类型为JavaScript对象或数 组,Jquery在提交之前自动调用JQuery.param()方法把要发送的数据编码成为"application/x-www-form- urlencoded"格式的数据(即 name=value&name1=value1);JavaScript对象必须为 Key/Value 格式;如果为数组,jQuery 将自动为不同值对应同一个名称。如 {foo:["bar1", "bar2"]} 转换为 '&foo=bar1&foo=bar2';若data数据类型为String类型,则直接默认该数据已经按照"application/x-www-form-urlencoded"格式编码完成,不再转换。processData选项可以控制是否进行转换。该选项默认为true。 |
dataType | String | 预期服务器返回的数据类型。设定HttpHeader中“Accept”域的内容,告诉服务器浏览器可以想要返回的数据格式类型,同时JQuery也会根据该类型对返回的数据进行处理。如果不指定,jQuery 将自动根据 HTTP 包 MIME 信息返回 responseXML 或 responseText,并作为回调函数参数传递,可用值:"xml": 返回 XML 文档,可用 jQuery 处理。"html": 返回纯文本 HTML 信息;包含 script 元素。"script": 返回纯文本 JavaScript 代码。不会自动缓存结果。"json": 返回 JSON 数据 。JQuery将返回的字符串格式数据自动转化为Javascript对象,便于直接使用obj.property格式访问。若没有指定该选项,即使返回的是JSON格式的字符串,JQuery也不会自动转换。"jsonp": JSONP 格式。使用 JSONP 形式调用函数时,如 "myurl?callback=?" jQuery 将自动替换 ? 为正确的函数名,以执行回调函数error Function (默认: 自动判断 (xml 或 html)) 请求失败时将调用此方法。这个方法有三个参数:XMLHttpRequest 对象,错误信息,(可能)捕获的错误对象。function (XMLHttpRequest, textStatus, errorThrown) {// 通常情况下textStatus和errorThown只有其中一个有值 this; // the options for this ajax request } |
global | Boolean | (默认: true) 是否触发全局 AJAX 事件。设置为 false 将不会触发全局 AJAX 事件,如 ajaxStart 或 ajaxStop 。可用于控制不同的Ajax事件 |
ifModified | Boolean | (默认: false) 仅在服务器数据改变时获取新数据。使用 HTTP 包 Last-Modified 头信息判断。 |
processData | Boolean | (默认: true) 默认情况下,发送的数据将被转换为对象(技术上讲并非字符串) 以配合默认内容类型 "application/x-www-form-urlencoded"。如果要发送 DOM 树信息或其它不希望转换的信息,请设置为 false。 |
success | Function | 请求成功后回调函数。这个方法有两个参数:服务器返回数据,返回状态function (data, textStatus) {// data could be xmlDoc, jsonObj, html, text, etc... this; // the options for this ajax request } |
一个例子。
var menuId = $( "ul.nav" ).first().attr( "id" );
var request = $.ajax({
url: "script.php",
method: "POST",
data: { id : menuId },
dataType: "html"
});
request.done(function( msg ) {
$( "#log" ).html( msg );
});
request.fail(function( jqXHR, textStatus ) {
alert( "Request failed: " + textStatus );
});
注意:The jqXHR.success(), jqXHR.error(), and jqXHR.complete() callbacks are removed as of jQuery 3.0. You can use jqXHR.done(), jqXHR.fail(), and jqXHR.always() instead.官方文档说明了在jquery3.0版本中,用了done.fail和always方法代替了原来的success,error,complete方法。
一个例子
$(document).ready(function(){
$("#search").click(function(){
$.ajax({
type: "GET",
url: "http://127.0.0.1:8080/ajaxdemo/serverjson2.php?number=" + $("#keyword").val(),
dataType: "json",
success: function(data) {
if (data.success) {
$("#searchResult").html(data.msg);
} else {
$("#searchResult").html("出现错误:" + data.msg);
}
},
error: function(jqXHR){
alert("发生错误:" + jqXHR.status);
},
});
});
$("#save").click(function(){
$.ajax({
type: "POST",
url: "serverjson.php",
data: {
name: $("#staffName").val(),
number: $("#staffNumber").val(),
sex: $("#staffSex").val(),
job: $("#staffJob").val()
},
dataType: "json",
success: function(data){
if (data.success) {
$("#createResult").html(data.msg);
} else {
$("#createResult").html("出现错误:" + data.msg);
}
},
error: function(jqXHR){
alert("发生错误:" + jqXHR.status);
},
});
});
});
对于ajax的jquery的学习,需要多加练习,遇到问题查看官网的解释以及网上博客。
serialize()方法接受不接受参数。
看一个官网的例子。
<form>
<select name="single">
<option>Single</option>
<option>Single2</option>
</select>
<br>
<select name="multiple" multiple="multiple">
<option selected="selected">Multiple</option>
<option>Multiple2</option>
<option selected="selected">Multiple3</option>
</select>
<br>
<input type="checkbox" name="check" value="check1" id="ch1">
<label for="ch1">check1</label>
<input type="checkbox" name="check" value="check2" checked="checked" id="ch2">
<label for="ch2">check2</label>
<br>
<input type="radio" name="radio" value="radio1" checked="checked" id="r1">
<label for="r1">radio1</label>
<input type="radio" name="radio" value="radio2" id="r2">
<label for="r2">radio2</label>
</form>
<p><tt id="results"></tt></p>
function showValues() {
//先把表单序列化,是一个字符串
var str = $( "form" ).serialize();
//在这个元素里面显示序列化的结果。
$( "#results" ).text( str );
}
//绑定事件,对于单选框和复选框
$( "input[type='checkbox'], input[type='radio']" ).on( "click", showValues );
$( "select" ).on( "change", showValues );
showValues();
JavaScript处于安全方面的考虑,不允许跨域调用其他页面的对象。
跨域简单的理解:由于JavaScript的同源策略,a.com域名下的js无法操作b.com或者c.a.com域名下的对象。
默认端口为80,一般省略掉。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.