Fundamental of JavaScript

完成了CSS/HTML的学习,接下来是第二本书《JavaScript DOM编程艺术(第2版)》。这本书被很多大神推荐,但是就我看完的感觉,不是很明白这本书为什么会这样受到热捧。这份笔记还结合了一部分廖雪峰的JavaScript教程,因为光凭这本书可能对JavaScript的基础语法还是没法很全面地掌握。后面还有几本JavaScript的pdf书,到时再作补充。

JavaScript基本语法


  1. 比较运算符=====
  • ==会自动转换数据类型再做比较,例如false == 0结果为真;
  • ===不做数据类型转换,直接比较,若类型不一致会直接返回false,例如false === 0为假;
    在Javascript相等判断时坚持使用===来比较。
  1. NaN
    Not a number,只能通过isNaN(value)来判断,直接比较NaN === NaN结果为假。
  2. 浮点数比较
    由于计算机的浮点数计算误差,使用Math.abs()相减的绝对值小于一个很小的值来判断浮点数的相等。
1
Math.abs(1 / 3 - (1 - 2 / 3)) < 0.0000001;
  1. 数组
  • JavaScript中的Array可以包含不同的元素。
1
var arr = [1, 2.2, 'xxx', true, null];
  • JavaScript中的Array可以越界存取,越界存时会引起length的扩大,越界取则取出undefined而不是报错终止运行;
  • indexOf(sth):可以用来搜索某个值的下标,若不存在则为-1;
  • slice(start, end):类似于substring,复制出子数组。若不指定开始和终止下标,默认将完整数组复制出来;
  • push和pop:向数组末尾加入若干或吐出一个元素,会更改length;若吐到没得吐了,返回undefined;
  • unshift和shift:向数组头部加入若干或吐出一个元素,会更改length;若吐到没得吐了,返回undefined;
  • splice:修改数组的万金油,可在指定位置删除、添加若干元素;
1
arr.splice(index, deleteNum, insertContent1, insertContent2, ...);
  • concat:将若干数组合并,复制到一个新的数组中而不是修改原数组。
1
arr.concat(1, 2, [3, 4]);
  • join:将当前数组的元素用给定字符连接起来,返回连接后的字符串。默认直接打印的数组以逗号连接。
  1. for循环
  • for(var i = 0; i < length; i++)循环最常用在遍历数组。
  • for...in用于将对象的所有属性依次遍历出来:
1
2
3
4
5
for (var key in o) {
if (o.hasOwnProperty(key)) {
alert(key); // 'name', 'age', 'city'
}
}

Array数组也是对象,使用for…in来遍历数组的时候,每个元素的索引被视为对象的属性(此时索引的类型被视为String)

1
2
3
4
5
var a = ['A', 'B', 'C'];
for (var i in a) {
alert(i); // '0', '1', '2'
alert(a[i]); // 'A', 'B', 'C'
}
  • for…of:针对iterable类型(Array, Map, Set)在EC6中加入,只循环遍历出集合本身的元素,而不是属性。
1
2
3
4
5
var a = ['A', 'B', 'C'];
a.name = 'Hello';
for (var x of a) {
alert(x); 'A', 'B', 'C'
}

此外对于iterable集合,还可以直接使用forEach函数。

1
2
3
array.forEach(function (元素, 索引, 本身) 
set.forEach(function (元素, 本身)
map.forEach(function (值, 键, 本身)

JavaScript的函数


  1. 函数的定义
  • 函数的方式:类似于c++/java的定义函数。
1
2
3
function abs(x) {
...
}
  • 变量的方式:利用赋值将一个函数定义赋值给一个变量,注意末尾的分号。
1
2
3
var abs = function(x) {
...
};
  1. 函数的调用
    JavaScript中的函数传入参数数目不符合定义不会报错。
    ->若传入的参数多于定义,只会从左到右取参数,取够为止;
    ->若传入参数不足,则不足的参数位置传入undefined,造成return的可能是NaN。
    所以为了防止参数是undefined,可以在函数开头处加上参数类型验证:
1
2
if (typeof x !== 'number')
throw 'Not a number';
  1. arguments关键字
    在函数内部使用的所有传入参数的集合,用法类似Array但不是Array。可用arguments.length来获得传入参数个数。
  2. rest伪参数
    在定义函数时,在参数列表最后加上try(a, b, …rest)就可以把多余的参数存入数组rest,方便在函数内部使用。
  3. this
    在’use strict’模式下,若this不是直接出现在函数内部,则会指向undefined。在非strict模式下则指向window全局对象。
  4. apply和call
  • 使用apply方法指定函数中的this指向哪个对象,传入需要this绑定的对象和对应的参数列表。
  • call方法也是用于绑定this,与apply区别在于参数是按顺序传入而不是打包传入。
1
2
Math.max.apply(null, [3, 5, 4]);
Math.max.call(null, 3, 5, 4);
  1. 高阶函数
    一个函数接收另一个函数作为参数。
  • map: 定义在JavaScript的Array中,可将传入的函数作用于数组的每一个元素,得到一个新的数组。
1
2
3
4
function pow(x) {
return x * x;
}
var result = arr.map(pow);
  • reduce: 定义在JavaScript的Array中,将传入的函数依次作用在数组的元素上,这个函数必须接受两个参数,把结果继续和序列的下一个元素累积运算,保存到一个新的数组。
1
2
3
4
// 使用reduce求和
var result = arr.reduce(function (x, y) {
return x + y;
});
  • filter: 定义在JavaScript的Array中,将传入的函数作用于数组的每一个元素,根据返回的布尔值是true还是false来决定保留还是舍弃该元素,最后将剩余元素返回,保存到一个新的数组。
1
2
3
4
// 将arr中的空字符串删掉
var result = arr.filter(function (s) {
return s && s.trim(); // 注意:IE9以下的版本没有trim()方法
});
  • sort: 定义在JavaScript的Array中,默认是将元素转为String再根据ASCII进行排序。可根据传入的函数(必须为两个参数)定义排序规则。排序结果直接影响原数组。
1
2
3
4
5
6
7
8
9
10
// 倒序排序
arr.sort(function (x, y) {
if (x < y) {
return 1;
}
if (x > y) {
return -1;
}
return 0;
});
  1. 闭包
  • 高阶函数不但可以将函数作为参数,还可以将函数作为结果值返回。当调用该高阶函数时,每次都会返回一个新生成的函数,这些函数之间的执行互不影响。
  • 闭包:将相关参数和变量都保存在返回的函数中。闭包可以用来返回一个函数并延迟执行,或封装一个私有变量,或将多参数函数变为单参数。(参考源)
1
2
3
4
5
6
7
8
9
function make_pow(n) {
return function (x) {
return Math.pow(x, n);
}
}

// 由make_pow创建两个新函数:
var pow2 = make_pow(2);
var pow3 = make_pow(3);
  1. 匿名函数与箭头函数
  • 匿名函数:无需赋予名字,定义后立即使用并只用一次。
1
(function (x) { return x * x }) (3);
  • 箭头函数:ES6加入,类似于匿名函数并简化了函数定义。但是箭头函数和匿名函数的区别在于箭头函数内部的this是词法作用域(外层调用者)。
1
2
3
4
5
(x, y) => x * x + y * y; 
// 相当于
function squareSum(x, y) {
return x * x + y * y;
}
  1. generator
  • 定义方式:语法和函数很像,只不过多了个星号function* foo(x) { ... }。除了return,中途还可以使用yield来返回多次。
  • 调用:直接用“函数名(参数列表)”的方式并不是调用,有两种方式调用。
    第一种是next()方法,每次遇到yield就返回一个对象{value: x, done: true/false},当done为真时意味着执行到最后的return了。
    第二种是用for…of的方式循环迭代generator对象,for(var x of fib(5))会把每次yield的value传给x直到结束。

JavaScript的对象


  1. 对象是由属性和方法组合构成的数据实体。其中
    属性property:属于某个特定对象的变量。
    方法method:某个特定对象才能调用的函数。
  2. 对象和实例
    实例之间各不相同,是对象的具体个体。为对象创建实例使用new关键字。
    其实我个人感觉实例和对象说的是一回事,类才是更抽象的一级。但是JavaScript中似乎没有提到类这个概念,所以就强行把对象和实例分开解释了?我一直觉得书里面说的对象,换成类也能说得通。(2016.8.15更新)实际上,在JavaScript中不区分类和实例的概念,而是通过原型来实现面向对象编程。在JavaScript中,所有对象都是实例,没有class的概念,所谓的继承关系也只是把一个对象的原型指向另一个对象而已。Bobby.__proto__ = Person; 但应该避免通过直接修改对象的原型实现继承关系。可以通过原型对象来创建实例对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 原型对象:
var Student = {
name: 'Robot',
height: 1.2,
run: function () {
console.log(this.name + ' is running...');
}
};
function createStudent(name) {
// 基于Student原型创建一个新对象:
var s = Object.create(Student);
// 初始化新对象:
s.name = name;
return s;
}
  1. 用户自定义对象
  • 键值无序集合
1
2
3
4
5
6
7
var person = {
firstname: "Bill",
lastname: "Gates",
age: 56,
eyecolor: "blue",
hasCar: true
};
  • 定义并创建对象的实例
1
2
3
4
5
6
7
8
person = new Object();
person.firstname="Bill";
person.lastname="Gates";
person.age=56;
person.eyecolor="blue";

// 或使用对象literals
person = {firstname:"John",lastname:"Doe",age:50,eyecolor:"blue"};
  • 使用函数来定义对象,然后创建新的对象实例。一定要使用关键词new才是调用构造函数,它绑定的this会指向新创建的对象,默认返回this(不要手动多加了return)。
1
2
3
4
5
6
function Person(firstname,lastname,age,eyecolor) {
this.firstname=firstname;
this.lastname=lastname;
this.age=age;
this.eyecolor=eyecolor;
}

构造对象时用new的方式var myFather=new Person("Bill","Gates",56,"blue");
注意这种情况下每个对象内如果定义了函数,这些函数虽然代码是相同,但属于不同的对象,浪费内存。解决方式是将各个对象共有的函数向上挪,挂到上一级的原型链上,例如Person.prototype.foo = function() {...};这样。

  1. 原型继承
    继承原本的含义是扩展一个已有的class,但是JavaScript中没有类的概念,这里的继承需要通过以下方式实现。
    (1) 定义新的构造函数,并在内部用call()调用希望继承的构造函数,并绑定this.
1
2
3
4
5
// PrimaryStudent构造函数:
function PrimaryStudent(props) {
Student.call(this, props);
this.grade = props.grade || 1;
}

(2) 借助空中间函数F实现原型链继承,最好通过封装的inherits函数完成可隐藏F。

1
2
3
4
5
6
function inherits(Child, Parent) {
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
}

(3) 继续在新的构造函数的原型上定义新方法。

1
2
3
4
// 绑定其他方法到PrimaryStudent原型:
PrimaryStudent.prototype.getGrade = function () {
return this.grade;
};
  1. Class继承
    从ES6开始引入class,让JavaScript引擎去实现原来需要我们自己编写的原型链代码。使用class继承和java相似,使用extends关键字:
1
2
3
4
5
6
7
8
9
10
class PrimaryStudent extends Student {
constructor(name, grade) {
super(name); // 记得用super调用父类的构造方法!
this.grade = grade;
}

myGrade() {
alert('I am at grade ' + this.grade);
}
}
  1. 内建对象
  • Javascript提供一些预先定义好的对象,拿来就用。
    例如数组,在var nuggets = new Array();的时候就是在创建一个Array对象的实例。
    而Math对象可以不实例化直接使用里面的方法var num = Math.round(8.245);
  • 但是其他包装对象不推荐使用,如new Number(), new String(), new Boolean(),这种情况下使用===会造成很大麻烦,所以还是直接用它们对应的普通类型比较好。
  1. 宿主对象
    由运行环境提供的对象,对于Web应用来说,运行环境就是浏览器,浏览器会提供Form, Image, Element, document, window等对象。

浏览器与DOM(Document Object Model文档对象模型)


  1. 浏览器提供的对象
  • window: 可获取浏览器窗口的尺寸,innerWidth, innerHeight等。
  • navigator: 可获取浏览器应用程序信息,如navigator.appName浏览器名, appVersion浏览器版本, platform操作系统, userAgent字段, language等。其中appName不是内核名称,根据W3C HTML5的规范,navigator对象的appName要么返回Netscape,要么返回浏览器的全名,这是为了兼容性而考虑的。
  • screen: 可获取设备屏幕信息,如screen.width, height, colorDepth颜色位数。
  1. DOM
  • D: 浏览器加载网页时,将html网页文档转化为文档对象。
  • O: 用户定义user-difined & 内建native & 宿主host三种对象。
  • M: 模型Model或地图Map。DOM代表这加载到浏览器窗口的当前网页,我们需要通过模型或地图来读取这个网页的组件。Xhtml文档节点树就是一种地图。
  1. 节点
  • 元素节点:DOM的原子是element node,就是节点树中的一个个元素,由标签定义。
  • 文本节点:文本内容。在html文档中,文本节点总是包含在元素节点的内部。
  • 属性节点:对元素进行更具体的描述,总是放在起始标签里。
  1. DOM方法获取元素节点
  • document.getElementById(“IdName”): 返回与给定id相同的唯一元素对象。是document对象特有的函数。
  • element.getElementsByTagName(“tagName”): 返回符合给定标签的元素的数组,不只有document可以调用,其他元素都可以调用,例如先使用ID拿到指定ul元素中的所有元素:var shopping = document.getElementById(“purchases”);然后再从这个ul元素中拿它包含的所有元素对象:var items = shopping.getElementsByTagName(“*”);
  • element.getElementsByClassName(“className”): 返回包含给定类名的元素的数组,给定的类名可以有多个,也是各种元素对象都可以调用的。但是必须指出HTML5 DOM才引入了这个方法,所以有时必须重载一个类似功能的方法用在旧的浏览器中,但是只能查询单一个类名:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getElementsByClassName(node, classname) {
if (node.getElementsByClassName) {
return node.getElementsByClassName(classname);
} else {
var results = new Array();
var elems = node.getElementsByTagName(“*”);
for (var i = 0; i < elems.length; i++) {
if (elems[i].className.indexOf(classname) != -1) {
results[results.length] = elems[i];
}
}
return results;
}
}
  • document.querySelector(‘selector’): 使用CSS选择器来获取节点。
  • element.querySelelctorAll(‘selector’): 与上面类似,不过返回的是所有符合条件的节点。
  1. DOM方法获取和设置元素属性
  • object.getAttribute(“attrName”): 返回给定属性的值,若找不到该属性,返回null。不能通过document调用,只能通过元素对象调用。
  • object.setAttribute(“attrName”): 对给定属性的值进行修改,若找不到该属性,会先创建该属性然后赋值。不能通过document调用,只能通过元素对象调用。如果不用DOM的方式来设置属性,可以直接通过object.attr = value;来赋值,但用途不如DOM广。
  1. 其他DOM属性
  • node.childNodes: 没有参数,直接获取元素的所有子元素的NodeList,返回保存着各种类型节点的数组(不仅仅是元素节点)。
  • element.children: 获取的是HTMLCollection,类似于上面的NodeList,区别在于children只能是element的属性,得到的仍是element;而childNodes则是Node的属性(只不过Element是Node的一种所以也可以使用childNodes属性),得到的是各种Node而不光是element。
  • node.nodeType: 返回数字值,指代该节点的类型,1-元素节点,2-属性节点,3-文本节点。
  • node.nodeName: 返回节点的标签名称,纯大写字母。
  • node.nodeValue: 可以获取或改变一个节点的值。注意对于例子<p id=”description”>Choose an image.</p>,若通过getElementById拿到p元素,此时直接调用nodeValue得到的是null,要想改变文本,其实需要通过childNodes[0]拿到第一个子元素,即文本元素,然后直接通过对nodeValue赋值进行修改。
  • node.firstNode: 完全等价于前面写的node. childNodes[0].
  • node.lastNode: 代表childNoes数组的最后一个元素,等价于node.childNodes[node.childNodes.length - 1];
  • node.parentElement: 获取父元素节点。
  • node.firstChild/lastChild: 获取的是Node,需要判断一下nodeType才能确定能否正确拿到元素节点。另有firstElementChild/lastElementChild可直接获取元素节点。
  • node.nextSibling/previousSibling/nextElementSibling/previousElementSibing: 与上面类似。
  1. 删除DOM
    node.removeChild(node): 直接删除,注意一旦删除就会对原有DOM产生影响,如果是用下标等方式来做要注意下标变化。

JavaScript操作表单和文件


  1. 表单控件
    包括了input输入框和select单选下拉框,前者又包括type=”text”文本框,”password”密码框,”radio”单选框,”checkbox”复选框,”hidden”隐藏携带文本。HTML5还加入了type=”date”,”datetime”,“datetime-local”,”color”。
  2. 获取/修改表单值
    使用value属性或checked属性。对于HTML5的标准控件,value一定是具有标准格式字符串。
  3. 提交表单
  • 使用form的submit()方法: 在HTML中,<form>之间的表单通过响应按钮控件的onclick事件实现表单提交,但这会干扰浏览器对form的正常提交。正常情况下浏览器默认点击submit类型的button来提交表单,或者在最后的input按回车键提交表单。
1
2
3
4
5
6
7
8
9
10
11
12
13
<form id="test-form">
<input type="text" name="test">
<button type=”button” onclick=”doSubmitForm()”>SUBMIT</button>
</form>

<script>
function doSubmitForm() {
var form = document.getElementById('test-form');
// 可以在此修改form的input...
// 提交form:
form.submit();
}
</script>
  • 响应form的onsubmit事件:在form标签后添加onsubmit响应事件。
1
2
3
4
5
6
7
8
9
10
11
12
13
<form id="test-form" onsubmit="return checkForm()">
<input type="text" name="test">
<button type="submit">Submit</button>
</form>

<script>
function checkForm() {
var form = document.getElementById('test-form');
// 可以在此修改form的input...
// 继续下一步:
return true;
}
</script>
  1. 上传文件
    在HTML表单中,只有<input type=”file”>可以控制上传文件,且只能由用户手动操作选择文件来上传,使用JavaScript对value赋值是没有效果的,且JavaScript也无法获得用户选择文件所在的真实路径。
    JavaScript可以在提交表单时对文件扩展名进行检查,防止文件格式不匹配。
1
2
3
4
5
6
var f = document.getElementById('test-file-upload');
var filename = f.value; // 'C:\fakepath\test.png'
if (!filename || !(filename.endsWith('.jpg') || filename.endsWith('.png') || filename.endsWith('.gif'))) {
alert('Can only upload image file.');
return false;
}
  1. HTML5文件操作FileAPI
    File API允许JavaScript读取文件内容,提供了File和FileReader两个对象获得文件信息。
    例如对于一个图片上传并预览的页面:
1
2
3
4
5
6
7
8
  <form method="post" action="http://localhost/test" enctype="multipart/form-data">
<p>图片预览:</p>
<p><div id="test-image-preview"</div></p>
<p>
<input type="file" id="test-image-file" name="test">
</p>
<p id="test-file-info"></p>
</form>

可以用JavaScript来检查文件是否选择、格式是否匹配图片、获取文件大小、修改时间等信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var
fileInput = document.getElementById('test-image-file'),
info = document.getElementById('test-file-info'),
preview = document.getElementById('test-image-preview');
// 监听change事件:
fileInput.addEventListener('change', function () {
// 清除背景图片:
preview.style.backgroundImage = '';
// 检查文件是否选择:
if (!fileInput.value) {
info.innerHTML = '没有选择文件';
return;
}
// 获取File引用:
var file = fileInput.files[0];
// 获取File信息:
info.innerHTML = '文件: ' + file.name + '<br>' +
'大小: ' + file.size + '<br>' +
'修改: ' + file.lastModifiedDate;
if (file.type !== 'image/jpeg' && file.type !== 'image/png' && file.type !== 'image/gif') {
alert('不是有效的图片文件!');
return;
}
// 读取文件:
var reader = new FileReader();
reader.onload = function(e) {
var
data = e.target.result; // 'data:image/jpeg;base64,/9j/4AAQSk...(base64编码)...'
preview.style.backgroundImage = 'url(' + data + ')';
};
// 以DataURL的形式读取文件:
reader.readAsDataURL(file);
});

其中使用了异步操作来读取文件内容。由于JavaScript是单线程执行,在执行多任务时需要异步调用,这里的reader.readAsDataURL(file)就发起了一个异步操作。它不会持续等待函数执行完毕才向后执行,而是利用前面的回调函数reader.onload = function(e);,读取文件成功后就是通过这个回调函数来安全地获取文件属性和内容。
6. Ajax异步执行网络请求(详见七•3
由于一次http请求对应取一个页面,如果当用户点击了submit跳转到新的页面而由于网速太慢会造成404找不到网页。要想让用户停留在当前页面的同时发送http请求,就需要用到JavaScript来发送请求,并根据返回的数据时对当前页面进行更新。用JavaScript写Ajax需要注意请求是异步执行的,需要通过回调函数获得响应。
PS: 这里的markdown语法用到了页内跳转,在后面加个span并给的id,这里就像添加链接一样[xx](#id)

最佳实践


  1. 平稳退化graceful degradation
    正确地使用JavaScript使得当访问者在题目拿到浏览器不支持JavaScript或手动关闭了js功能的情况下仍能顺利浏览网站。
    例如使用window.open(“url”, “name”, “features”)创建弹出窗口,使用JavaScript构建一个函数:
1
2
3
function popUp(winURL) {
window.open(winURL, “popup”, “width=320, height=480”);
}

在html中使用这个函数可以用三种方法:

  • 伪协议: <a href=”javascript:popUp(“http://xxx/”);”>xxxxxx</a>。这样的语句在支持JavaScript伪协议的浏览器中可以正常工作,但是旧的或关了js的就不行了。
  • 内嵌事件处理函数:<a href=”#” onclick=”popUp(“http://xxx/”); return false;”>xxxxx</a>一样无法支持不执行JavaScript的浏览器。
  • 合法链接backup:<a href=”http://xxx/” onclick=”popUp(this.href); return false;”>xxxxx</a>这样会优先执行JavaScript,失败后仍可以直接打开URL,虽然无法保留在当前页面弹窗打开,但至少不会点击了没有反应。
  1. 分离JavaScript
    类似于CSS样式与html元素分离,对于元素的事件也可以通过外部js文件加入到元素标签中:element.event = action。需要注意的是将JavaScript代码放到外部文件后,里面的document.getElement...函数可能无法正常工作,因为引用JavaScript时DOM模型可能还没有成型,浏览器还没有把所有元素都加载进来,那么可能尚且找不到对应的元素。为了让HTML加载完再触发js,添加一个window.onload来确保DOM已经成型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
window.onload = prepareLinks;
function prepareLinks() {
var links = document.getAttributeByTagName(“a”);
for (var i = 0; i < links.length; i++) {
if (links[i].getAttribute(“class”) == “popup”) {
links[i].onclick = function() {
popUp(this.getAttribute(“href”));
return false;
}
}
}
}

fucntion popUp(winURL) {
window.open(winURL, “popup”, ”width=320, height=480”);
}

共享onload事件可以利用一个addLoadEvent函数,传入打算在页面加载完毕立即执行的函数的名字,绑定到onload事件上。若onload已经绑定有函数,则先执行完先前的函数再执行当前函数。若持续调用,则会为这一系列函数建队列,排队依次执行:

1
2
3
4
5
6
7
8
9
function addLoadEvent(func) {
var oldOnload = window.onload;
if (typeof window.onload != ‘function’) {
window.onload = func;
} else {
oldOnload();
func();
}
}
  1. 向后兼容
  • 对象检测
    直接检测对象是否可用,即用if语句发现该方法无法使用,就不继续执行了。
1
2
3
4
5
window.onload = function() {
if (!document.getElementsByTagName) return false;
var links = document.getElementsByTagName(“a”);
....
}
  • 浏览器嗅探
    检测浏览器种类和版本来判断函数是否可用。我感觉这个方法不如直接检测对象有用。
  1. 性能考虑
  • 尽可能少访问DOM,例如多用变量存储节点而不是反复getElementsxxx。
  • 合并脚本,尽量减少加载页面时发送的请求数量,不用总是请求小js文件。
  • 脚本引用标签script放在文档末尾、body结束之前,因为HTTP规范中浏览器同时只能从同一域名下载两个文件,若放在head中则浏览器在下载脚本期间不会下载任何其他文件(即使是不同域名),这可能阻塞其他资源的加载严重影响性能。
  • 压缩脚本,即保存一份xxx.min.js的文件把里面所有的空格、注释、换行都删除。但我觉得这个有点反人类,万一改出错了想纠正会特别困难。按照这本书的说法,需要保存两份代码,一份是人类能看的,一类是经过工具压缩的代码。

动态创建元素


  1. 传统方法回顾
  • document.write: 使用JavaScript代码向HTML中动态创建、插入元素。例如在JavaScript中使用document.write(“<p>sth</p>”);插入元素。这违背了“行为与表现分离”的原则,在想要插入的地方需要借助script标签才能调用相应的代码。将html和js的代码混在一起是很糟糕的做法。
  • innerHTML属性: 用于读写给定元素里的HTML内容。例如document.getElementById(“testdiv”).innerHTML就可以获得HTML文件中id为testdiv元素内部的HTML纯文本内容;documnet.getElementById(“testdiv”).innerHTML = “<p>New content</p>”就可以修改里面的HTML纯文本,实现动态修改元素,当原元素为空时可以实现插入元素。这种方法可以把js代码和html分离开来。
  • innerText/textContent属性:会自动对文本内容进行html编码,无法插入新标签。而读取时前者不返回隐藏标签,后者返回所有文本。
  1. DOM方法
    DOM方法的本质是在改变文档节点树。具体来讲,首先需要创建一个新元素,然后将该元素插入现有文档节点树中,接着根据需要追加插入文本节点。
  • document.createElement(“nodeName”):调用后会创建一个孤立的元素(文档碎片,document fragment),但是并没有连接到DOM节点树上。
  • parent.appendChild(child):会将变量child作为子节点插入到parent节点下面,真正连到DOM文档中。
  • document.createTextNode(“text”):创建文本节点,可结合上一个函数插入到现有元素的子节点完成文本插入。
  • parentElement.insertBefore(newElement, targetElement):在现有元素之前插入新元素,其中parentElement不必真的去找出来,可以直接调用targetElement.parenetNode获得父节点。
  • 在现有元素之后插入新元素:DOM其实并没有提供,但是完全可以自己编写:
1
2
3
4
5
6
7
8
function insertAfter(newElement, targetElement) {
var parent = targetElement.parentNode;
if (targetElement == parent.lastChild) {
parent.appendChild(newElement);
} else {
parent.insertBefore(newElement, targetElement.nextSibling);
}
}
  1. Ajax异步加载
  • 以往点击某个链接刷新局部信息时,请求发送到服务器,然后服务器根据操作返回完整的新页面,这十分浪费资源。而Ajax可以只刷新加载网页的局部,对页面的请求以异步的方式发送到服务器,服务器会在后台处理请求,用户仍然可以继续浏览页面并进行交互。但Ajax必须依赖javascript;而且Ajax相应地也缺少状态记录,无法按照用户习惯进行后退、收藏等操作。同时只更新部分区域可能也会影响用户预期,所以Ajax响应必须给出明确的提示。
  • 异步请求的特点:脚本发送XMLHttpRequest请求之后,即调用send函数后仍会继续执行后续脚本,不会一直等待响应返回。响应返回后,所有依赖于服务器响应的操作才会执行。
  • XMLHttpRequest对象:以往请求都由浏览器发出,而如今javascript可以利用这个对象自己发送请求、处理响应,这个对象充当浏览器脚本(客户端)和服务器之间的中间人的角色。
  • var request = getHTTPObject(): 获取XMLHttpRequest对象,需要考虑浏览器版本兼容问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
function getHTTPObjecct() {
if (typeof XMLHttpRequest == “undefined”)
XMLHttpRequest = function() {
try { return new ActiveXObject(“Msxml2.XMLHTTP.6.0”); }
catch (e) {}
try { return new ActiveXObject(“Msxml2.XMLHTTP.3.0”); }
catch (e) {}
try { return new ActiveXObject(“Msxml2.XMLHTTP”); }
catch (e) {}
return false;
}
return new XMLHttpRequest();
}
  • getNewContent: 手写函数,利用XMLHttpRequest对象的open方法指定服务器上要访问的文件、指定请求类型GET, POST, SEND,并指定是否以异步方式发送和处理请求;接着利用onreadystatechage属性来指定处理响应的函数;最后将请求异步发送,直接调用send。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function getNewContent() {
var request = getHTTPObject();
if (request) {
request.open(“GET”, “xxx.txt”, true);
request.onreadystatechange = function() {
if (request.readyState == 4) {
var para = document.createElement(“p”);
var txt = document.createTextNode(request.responseText);
para.appendChild(txt);
document.getElementById(“new”).appendChild(para);
};
request.send(null);
} else {
Alert(“Sorry, your browser doesn\’t support XMLHttpRequest”);
}
}
}
addLoadEvent(getNewContent);

其中request.onredaystatechange是获取readyState属性的函数,有五个取值,0-未初始化,1-正在加载,2-加载完毕,3-正在交互,4-已完成,我们在上面函数中拿到4就说明可以访问服务器返回来的数据了。返回的数据可以使用两个函数来处理,一是request.responseText属性,它保存了文本字符串的数据;二是request.responseXML属性,它保存了Content-Type头部中指定为”texxt/xml”的数据。
4. Hijax
由于Ajax要求必须启用JavaScript才能正常使用Ajax访问内容,这会导致网站可用性和可访问性的问题。最好的做法是先构建一个常规的网站,然后Hijax它,拦截正常发往服务器的请求并转交XMLHttpRequest对象处理。Hijax的意思是“渐进增强地使用Ajax”.
Ajax应用依赖后台服务器,因为是后台服务器的脚本完成了大部分的工作。XMLHttpRequest对象作为浏览器与服务器之间的“中间人”只负责不阻塞地传递请求和响应而已。在不支持Ajax的浏览器上,这个中间人挪开应该有传统方式保证相似的请求和响应仍能正常工作,只不过不是异步、局部请求而已,耗时稍长但不至于不可用。

CSS-DOM


  1. 网页是三层信息构成的共同体
  • 结构层:由html或xhtml标记语言创建,对网页的语义含义做出描述,但标签并不包含任何关于内容如何显示的信息。
  • 表示层:由CSS描述页面内容如何呈现。
  • 行为层:决定内容应该如何响应事件,由javascript+DOM控制。
  • 分离与交叉:正常来说,应该严格选择最恰当的工具去解决问题,在网页设计中这意味着用html搭建文档结构、用CSS设置文档呈现的效果、用DOM脚本实现文档的行为。但是三者之间存在着重叠的区域,例如CSS利用伪类:hover, :focus也可以控制触发元素样式改变,js-DOM也可以给元素设置样式。
  1. style属性
  • 获取style属性:每个元素节点都有style属性,需要注意的是用element.style返回的是一个style对象,如果要访问font-family属性,需要element.style.fontFamily这样的驼峰命名方式来访问或修改。但style属性只能获取内嵌样式,即在元素标签内带有style属性指明的样式才能拿到,否则就是Null,而我们知道结构应与样式分离,所以用DOM方式来获取样式其实没有多少实用价值。
  • 修改style属性:部分DOM属性如previousSibling, nextSibling, parentNode, firstChild, lastChiild都是只读的,而style属性则是可以赋值的。element.style.property = “value”,值必须用引号包围,否则就相当于变量了。
  1. 在什么情况下才选择使用DOM脚本设置样式
  • 根据元素在节点树中的位置来设置样式:由于CSS无法根据元素之间的相对位置关系找出某个特定元素进行设置(虽然CSS3有一些位置选择器不过浏览器不一定支持),可以用DOM找出文档中指定元素。
  • 根据某种条件反复设置某种样式:例如使table表格行与行之间交替显示不同颜色,在CSS3的nth-child(even/odd)选择器用不了的时候,如果非要用CSS实现就必须为奇数和偶数行分别加class再设置样式,万一表格很大又要修改那就悲剧了。而使用DOM就可以动态找出奇数偶数行。
  • 用样式响应事件:CSS中a标签可以用hover控制鼠标悬停的样式,但对于其他元素要想设置悬停样式就得用DOM方式的onmouseover来实现了。
  1. className属性
    既然使用DOM直接修改样式不是好的工作方式,那么我们可以修改class,依然由CSS去决定样式。凡是元素节点都有className属性,若是直接element.className=value赋值,则原有的CSS样式将完全被新的class指定的样式覆盖(而不是叠加)。要想叠加类名,则应该先判断原先有没有class,若有则应当把空格+新类名追加到className上:
1
2
3
4
5
6
7
8
9
10
function addClass(element, value) {
if (!element.className) {
element.className = value;
} else {
var newClassName = element.className;
newClassName += “ “;
newClassName += value;
element.className = newClassName;
}
}
  1. 对函数的抽象
    把一个具体的东西改进为一个较为通用的东西的过程叫抽象。例如为h1元素添加intro这个类,编写的函数为:
1
2
3
4
5
6
7
8
function styleHeaderSiblings() {
if (!document.getElementsByTagName) return false;
var headers = document.getElementsByTagName(“h1”);
var elem;
for (var i = 0; i < headers.length; i++) {
elem = getNextElement(headers[i].nextSibling);
addClass(elem, “intro”);
}

这个函数就不够抽象。抽象的函数具有普适性,上面这个函数完全可以把h1和intro分别作为参数传入这个函数,这样就可以应用到其他情况。

Javascript实现动画


  1. 利用javascript按照预定的时间间隔重复调用一个函数,从而让某个元素的样式随着时间的推移不断改变。这是CSS无能为力的。例如让一条message在屏幕上渐渐移动:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function moveElement(elementID, final_x, final_y, interval) {
if (!document.getElementById) return false;
if (!document.getElementById(elementID)) return false;
var elem = document.getELementById(elementID);
var xpos = parseInt(elem.style.left);
var ypos = parseInt(elem.style.top);
if (xpos == final_x && ypos == final_y)
return true
if (xpos < final_x)
xpos++;
if (xpos > final_x)
xpos--;
if (ypos < final_y)
ypos++;
if (ypos > final_y)
ypos--;
elem.style.left = xpos + “px”;
elem.style.top = ypos + “px”;
var repeat = “moveElement(‘” + elementID + “’,” + final_x + “,” + final_y + “,” + interval + “)”;
movement = setTimeout(repeat, interval);
}

需要指出的是,上面的函数调用了elem.style来获取元素当前位置,这意味着该元素必须通过内嵌样式style属性或已经通过DOM植入了style属性,否则无法获取位置。
2. 实际中的动画
Javascript有些臭名昭著就是因为加入的动画引起访问者的反感,导致可访问性问题。除非浏览器允许用户停止移动的内容,否则就应避免让内容在页面中移动。实际中用得比较多的动画并不是元素在屏幕上移动,而是例如加载一个多图标的完整图片,根据用户鼠标的位置在图标框中显示不同的内容(有点类似spirte图)。