你不知道的JS
-
作用域
作用域为可访问变量,对象,函数的集合。
抛开遮蔽效应,作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。 -
最小特权原则/最小暴露原则
应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的API设计。 -
IIFE(Immediately Invoked Function Expression)
立即执行函数表达式。函数被包含在一对()括号内部,因此成为了一个表达式,通过在末尾加上另外一个()可以立即执行这个函数。 -
let关键字可以将变量绑定到所在的任意作用域中(通常是{..}内部)。let 为其声明的变量隐式地附加在了所在的块作用域。
但是使用 let 进行的声明不会在块作用域中进行提升。声明的代码被运行之前,声明并不“存在”。 - 当你看到 var a = 2; 时,可能会认为这是一个声明。但JavaScript实际上会将其看成两个声明: var a; 和 a = 2; 。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。
只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。 - 函数声明和变量声明都会被提升。函数会首先被提升,然后才是变量。尽管重复的 var 声明会被忽略掉,但出现在后面的函数声明还是可以覆盖前面的。
- 当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
(闭包就是一个函数引用另一个函数的变量,因为变量被引用着所以不会被回收,因此可以用来封装一个私有变量。这是优点也是缺点,不必要的闭包只会增加内存消耗。)
无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。 - 模块有两个主要特征:(1)为创建内部作用域而调用了一个包装函数;(2)包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。
- 词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的。( this也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。事实上JavaScript并不具有动态作用域。它只有词法作用域,简单明了。但是 this 机制某种程度上很像动态作用域。
- 箭头函数在涉及 this 绑定时的行为和普通函数的行为完全不一致。它放弃了所有普通 this 绑定的规则,取而代之的是用当前的词法作用域覆盖了 this 本来的值。箭头函数的绑定无法被修改。( new 也不行!)
- 需要明确的是, this 在任何情况下都不指向函数的词法作用域。 this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。 this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
- 当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。
- 存储在对象容器内部的是这些属性的名称,它们就像指针(从技术角度来说就是引用)一样,指向这些值真正的存储位置。
-
JSON安全
也就是说可以被序列化为一个JSON字符串并且可以根据这个字符串解析出一个结构和值完全一样的对象。
所有安全的 JSON 值(JSON-safe)都可以使用 JSON.stringify(..) 字符串化。 - ES6定义了 Object.assign(..) 方法来实现浅复制。由于Object.assign(..) 就是使用 = 操作符来赋值,所以源对象属性的一些特性(比如 writable)不会被复制到目标对象。
- 对象默认的 [[Put]] 和 [[Get]] 操作分别可以控制属性值的设置和获取。
- in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中。相比之下, hasOwnProperty(..) 只会检查属性是否在 myObject 对象中,不会检查 [[Prototype]] 链。
看起来 in 操作符可以检查容器内是否有某个值,但是它实际上检查的是某个属性名是否存在。 - Object.keys(..) 会返回一个数组,包含所有可枚举属性, Object.getOwnPropertyNames(..) 会返回一个数组,包含所有属性,无论它们是否可枚举。
可枚举就相当于“可以出现在对象属性的遍历中”。 - 面向对象编程强调的是数据和操作数据的行为本质上是互相关联的。
- 继承意味着复制操作,JavaScript(默认)并不会复制对象属性。相反,JavaScript会在两个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数。
- 调用 Object.create(..)会凭空创建一个“新”对象并把新对象内部的[[Prototype]] 关联到你指定的对象。
- ES6添加了辅助函数 Object.setPrototypeOf(..) ,可以用标准并且可靠的方法来修改关联。
- 原型链的作用是:如果在对象上没有找到需要的属性或者方法引用,引擎就会继续在 [[Prototype]] 关联的对象上进行查找。
- 带 new 的函数调用通常被称为“构造函数调用”。通过构造函数(如 new String("abc") )创建出来的是封装了基本类型值(如 "abc" )的封装对象。
- 虽然这些JavaScript机制和传统面向类语言中的“类初始化”和“类继承”很相似,但是JavaScript中的机制有一个核心区别,那就是不会进行复制,对象之间是通过内部的 [[Prototype]] 链关联的。
- The Object.create() method creates a new object, using an existing object as the prototype of the newly created object.
The isPrototypeOf() method checks if an object exists in another object's prototype chain. - 所有 typeof 返回值为 "object" 的对象(如数组)都包含一个内部属性 [[Class]]。这个属性无法直接访问,一般通过Object.prototype.toString(..) 来查看。
Object.prototype.toString.call( [1,2,3] );
// "[object Array]"
Object.prototype.toString.call( /regex-literal/i );
// "[object RegExp]"
JS高级程序设计
- DOM(文档对象模型,Document Object Model) 把整个页面映射为一个多层节点结构。HTML或 XML 页面中的每个组成部分都是某种类型的节点,这些节点又包含着不同类型的数据。
- 在文档的 <head> 元素中包含所有 JavaScript 文件,意味着必须等到全部 JavaScript 代码都被下载、解析和执行完成以后,才能开始呈现页面的内容(浏览器在遇到 <body> 标签时才开始呈现内容)。对于那些需要很多 JavaScript 代码的页面来说,这无疑会导致浏览器在呈现页面时出现明显的延迟,而延迟期间的浏览器窗口中将是一片空白。为了避免这个问题,现代 Web 应用程序一般都把全部 JavaScript 引用放在 <body> 元素中页面内容的后面。
- <noscript> 元素,用以在不支持 JavaScript 的浏览器中显示替代的内容。
包含在 <noscript> 元素中的内容只有在下列情况下才会显示出来:
- 浏览器不支持脚本;
- 浏览器支持脚本,但脚本被禁用。
符合上述任何一个条件,浏览器都会显示 <noscript> 中的内容。而在除此之外的其他情况下,浏览器不会呈现 <noscript> 中的内容。
- 用 var 操作符定义的变量将成为定义该变量的作用域中的局部变量。也就是说,如果在函数中使用 var 定义一个变量,那么这个变量在函数退出后就会被销毁。
- 从逻辑角度来看, null 值表示一个空对象指针,而这也正是使用 typeof 操作符检测 null 值时会返回 "object" 的原因。
- NaN 与任何值都不相等,包括 NaN 本身。
- 用 parseInt() 转换空字符串会返回 NaN;Number() 对空字符返回 0。
- 按位非操作的本质:操作数的负值减 1。
- break 语句会立即退出循环,强制继续执行循环后面的语句。而 continue 语句虽然也是立即退出循环,但退出循环后会从循环的顶部继续执行。
- 基本数据类型: Undefined 、 Null 、 Boolean 、 Number 和 String、Symbol。
- 基本类型值指的是简单的数据段,而引用类型值指那些可能由多个值构成的对象。
- 当从一个变量向另一个变量复制引用类型的值时,同样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。
访问变量有按值和按引用两种方式,而参数只能按值传递。
可以把 ECMAScript 函数的参数想象成局部变量。 - 在 Web 浏览器中,全局执行环境被认为是 window 对象。
- 内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。这些环境之间的联系是线性、有次序的。每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。
- 一旦数据不再有用,最好通过将其值设置为 null 来释放其引用——这个做法叫做解除引用(dereferencing)。这一做法适用于大多数全局变量和全局对象的属性。局部变量会在它们离开执行环境时自动被解除引用。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。
- 变量、作用域和内存问题总结
- 基本类型值在内存中占据固定大小的空间,因此被保存在栈内存中;
- 引用类型的值是对象,保存在堆内存中;
- 从一个变量向另一个变量复制引用类型的值,复制的其实是指针,因此两个变量最终都指向同一个对象;
- 每次进入一个新执行环境,都会创建一个用于搜索变量和函数的作用域链;
- 函数的局部环境不仅有权访问函数作用域中的变量,而且有权访问其包含(父)环境,乃至全局环境;
- 离开作用域的值将被自动标记为可以回收,因此将在垃圾收集期间被删除。
- 构造函数本身就是一个函数,只不过该函数是出于创建新对象的目的而定义的。
- 在对象字面量中,使用逗号来分隔不同的属性。其中数值属性名会自动转换成字符串。
var person = {
"name" : "Nicholas",
"age" : 29,
5 : true
};
- 每个函数都是 Function 类型的实例,而且都与其他引用类型一样具有属性和方法。由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。
- this引用的是函数据以执行的环境对象——或者也可以说是 this 值(当在网页的全局作用域中调用函数时,this 对象引用的就是 window )。
- apply() 和 call() 。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内 this 对象的值。首先, apply() 方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。对于 call()方法而言,第一个参数是 this 值没有变化,变化的是其余参数都直接传递给函数(逗号隔开)。
- bind() 这个方法会创建一个函数的实例,其 this 值会被绑定到传给 bind() 函数的值。
- 要找到数组中的最大或最小值,可以像下面这样使用apply() 方法。
var values = [1, 2, 3, 4, 5, 6, 7, 8];
var max = Math.max.apply(Math, values);
- Math.ceil() 执行向上舍入,即它总是将数值向上舍入为最接近的整数;
- Math.floor() 执行向下舍入,即它总是将数值向下舍入为最接近的整数;
- Math.round() 执行标准舍入,即它总是将数值四舍五入为最接近的整数
-
原型模式
我们创建的每个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。
只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。 - 当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。
-
原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此。
调用构造函数时会为实例添加一个指向最初原型的[[Prototype]] 指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造函数。 - 组合使用构造函数模式和原型模式
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。
// 构造函数模式
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
// 原型模式
Person.prototype = {
constructor : Person,
sayName : function(){
alert(this.name);
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
实例属性都是在构造函数中定义的,而由所有实例共享的属性 constructor 和方法 sayName() 则是在原型中定义的。而修改了 person1.friends (向其中添加一个新字符串),并不会影响到 person2.friends ,因为它们分别引用了不同的数组。
- 用原型链实现继承的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。这样,子类型就能够访问超类型的所有属性和方法。
- 构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
- 所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.prototype 。这也正是所有自定义类型都会继承 toString() 、valueOf() 等默认方法的根本原因。
- 原型链的问题:a) 最主要的问题来自包含引用类型值的原型。在通过原型来实现继承时,原型实际上会变成另一个类型的实例; b) 没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
- 相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。
function SuperType(name){
this.name = name;
}
function SubType(){
//继承了 SuperType,同时还传递了参数
SuperType.call(this, "Nicholas");
//实例属性
this.age = 29;
}
var instance = new SubType();
alert(instance.name); //"Nicholas";
alert(instance.age); //29
- 组合继承,其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(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(){
alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27
组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。
- 寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
function createAnother(original){
var clone = object(original); //通过调用函数创建一个新对象
clone.sayHi = function(){ //以某种方式来增强这个对象
alert("hi");
};
return clone; //返回这个对象
}
使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。
- 寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
- 关于函数声明,它的一个重要特征就是函数声明提升(function declaration hoisting),意思是在执行代码之前会先读取函数声明。
- 闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。因为创建闭包必须维护额外的作用域,所以过度使用它们可能会占用大量内存。
- 当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链。然后,使用 arguments 和其他命名参数的值来初始化函数的活动对象(activation object)。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至作为作用域链终点的全局执行环境。
作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。 - this 对象是在运行时基于函数的执行环境绑定的:在全局函数中, this 等于 window ,而当函数被作为某个对象的方法调用时,this 等于那个对象。
- BOM(浏览器对象模型) 的核心对象是 window ,它表示浏览器的一个实例。在浏览器中, window 对象有双重角色,它既是通过 JavaScript 访问浏览器窗口的一个接口,又是 ECMAScript 规定的 Global 对象。这意味着在网页中定义的任何一个对象、变量和函数,都以 window 作为其 Global 对象,因此有权访问parseInt() 等方法。
- 超时调用需要使用 window 对象的 setTimeout() 方法,它接受两个参数:要执行的代码和以毫秒表示的时间(即在执行代码前需要等待多少毫秒)。
调用 setTimeout() 之后,该方法会返回一个数值 ID,表示超时调用。这个超时调用 ID 是计划执行代码的唯一标识符,可以通过它来取消超时调用。要取消尚未执行的超时调用计划,可以调用clearTimeout() 方法并将相应的超时调用 ID 作为参数传递给它。
//设置超时调用
var timeoutId = setTimeout(function() {
alert("Hello world!");
}, 1000);
//注意:把它取消
clearTimeout(timeoutId);
- 间歇调用与超时调用类似,只不过它会按照指定的时间间隔重复执行代码,直至间歇调用被取消或者页面被卸载。设置间歇调用的方法是 setInterval()。
调用 setInterval() 方法同样也会返回一个间歇调用 ID,该 ID 可用于在将来某个时刻取消间歇调用。要取消尚未执行的间歇调用,可以使用 clearInterval() 方法并传入相应的间歇调用 ID。取消间歇调用的重要性要远远高于取消超时调用,因为在不加干涉的情况下,间歇调用将会一直执行到页面卸载。
var num = 0;
var max = 10;
var intervalId = null;
function incrementNumber() {
num++;
//如果执行次数达到了 max 设定的值,则取消后续尚未执行的调用
if (num == max) {
clearInterval(intervalId);
alert("Done");
}
}
intervalId = setInterval(incrementNumber, 500);
- 在使用超时调用时,没有必要跟踪超时调用 ID,因为每次执行代码之后,如果不再设置另一次超时调用,调用就会自行停止。一般认为,使用超时调用来模拟间歇调用的是一种最佳模式。在开发环境下,很少使用真正的间歇调用,原因是后一个间歇调用可能会在前一个间歇调用结束之前启动。而像前面示例中那样使用超时调用,则完全可以避免这一点。所以,最好不要使用间歇调用。
- location 对象是很特别的一个对象,因为它既是 window 对象的属性,也是document 对象的属性;换句话说, window.location 和 document.location 引用的是同一个对象。location 对象的用处不只表现在它保存着当前文档的信息,还表现在它将 URL 解析为独立的片段,让开发人员可以通过不同的属性访问这些片段。
-
DOM(文件对象模型)是针对 HTML 和 XML 文档的一个 API(应用程序编程接口)。DOM 描绘了一个层次化的节点树,允许开发人员添加、移除和修改页面的某一部分。
DOM 可以将任何 HTML 或 XML 文档描绘成一个由多层节点构成的结构。节点分为几种不同的类型,每种类型分别表示文档中不同的信息及(或)标记。每个节点都拥有各自的特点、数据和方法,另外也与其他节点存在某种关系。节点之间的关系构成了层次,而所有页面标记则表现为一个以特定节点为根节点的树形结构。 - 文档节点是每个文档的根节点。其子节点<html>元素,我们称之为文档元素。文档元素是文档的最外层元素,文档中的其他所有元素都包含在文档元素中。每个文档只能有一个文档元素。在 HTML 页面中,文档元素始终都是 <html> 元素。
- 每个节点都有一个 childNodes 属性,其中保存着一个 NodeList 对象。 NodeList 是一种类数组对象,用于保存一组有序的节点,可以通过位置来访问这些节点。请注意,虽然可以通过方括号语法来访问 NodeList 的值,而且这个对象也有 length 属性,但它并不是 Array 的实例。
- 对 arguments 对象使用 Array.prototype.slice() 方法可以将其转换为数组。而采用同样的方法,也可以将 NodeList 对象转换为数组。Array.prototype.slice.call()详解
//在 IE8 及之前版本中无效
var arrayOfNodes = Array.prototype.slice.call(someNode.childNodes,0);
- 每个节点都有一个 parentNode 属性,该属性指向文档树中的父节点。
列表中第一个节点的 previousSibling 属性值为 null ,而列表中最后一个节点的 nextSibling 属性的值同样也为 null。 - appendChild() ,用于向 childNodes 列表的末尾添加一个节点。
- Document 另一个可能的子节点是 DocumentType 。通常将 <!DOCTYPE> 标签看成一个与文档其他部分不同的实体,可以通过 doctype 属性(在浏览器中是 document.doctype )来访问它的信息。
- NodeList 对象都是“动态的”,这就意味着每次访问NodeList 对象,都会运行一次查询。有鉴于此,最好的办法就是尽量减少 DOM 操作。
-
querySelector() 方法接收一个 CSS 选择符,返回与该模式匹配的第一个元素,如果没有找到匹配的元素,返回 null 。
querySelectorAll() 方法接收的参数与 querySelector() 方法一样,都是一个 CSS 选择符,但返回的是所有匹配的元素而不仅仅是一个元素。这个方法返回的是一个 **NodeList **的实例。
getElementsByClassName() 方法接收一个参数,即一个包含一或多个类名的字符串,返回带有指定类的所有元素的 NodeList 。 - HTML5 新增了一种操作类名的方式,可以让操作更简单也更安全,那就是为所有元素添加classList 属性。此属性有如下方法:add(), contains(), remove(), toggle()。
- document.activeElement 属性,这个属性始终会引用 DOM 中当前获得了焦点的元素。通过检测文档是否获得了焦点,可以知道用户是不是正在与页面交互。
var button = document.getElementById("myButton");
button.focus();
alert(document.activeElement === button); //true
- 在写模式下, innerHTML 会根据指定的值创建新的 DOM树,然后用这个 DOM 树完全替换调用元素原先的所有子节点。
通过 innertText属性可以操作元素中包含的所有文本内容,包括子文档树中的文本。 - 事件就是用户或浏览器自身执行的某种动作。诸如 click 、 load 和 mouseover ,都是事件的名字。而响应某个事件的函数就叫做事件处理程序(或事件侦听器)。事件处理程序的名字以 "on" 开头,因此click 事件的事件处理程序就是 onclick , load 事件的事件处理程序就是 onload 。
在 HTML 中指定事件处理程序有两个缺点。首先,存在一个时差问题。因为用户可能会在HTML 元素一出现在页面上就触发相应的事件,但当时的事件处理程序有可能尚不具备执行条件。另一个缺点是,这样扩展事件处理程序的作用域链在不同浏览器中会导致不同结果。通过 HTML 指定事件处理程序的最后一个缺点是 HTML 与 JavaScript 代码紧密耦合。如果要更换事件处理程序,就要改动两个地方:HTML 代码和 JavaScript 代码。而这正是许多开发人员摒弃 HTML 事件处理程序,转而使用 JavaScript 指定事件处理程序的原因所在。 - 要使用 JavaScript 指定事件处理程序,首先必须取得一个要操作的对象的引用。
每个元素(包括 window 和 document )都有自己的事件处理程序属性,这些属性通常全部小写,例如 onclick 。将这种属性的值设置为一个函数,就可以指定事件处理程序, - “DOM2级事件”定义了两个方法,用于处理指定和删除事件处理程序的操作:addEventListener()和 removeEventListener() 。
使用 DOM2 级方法添加事件处理程序的主要好处是可以添加多个事件处理程序。
通过 addEventListener() 添加的事件处理程序只能使用 removeEventListener() 来移除;移除时传入的参数与添加处理程序时使用的参数相同。这也意味着通过 addEventListener() 添加的匿名函数将无法移除。 -
preventDefault(): 取消事件的默认行为。如果cancelable是true,则可以使用这个方法。
stopPropagation(): 取消事件的进一步捕获或冒泡,如果bubbles为true,则可以使用这个方法。(用于立即停止事件在DOM层次中的传播。) - 鼠标与滚轮事件:
- mousedown :在用户按下了任意鼠标按钮时触发。不能通过键盘触发这个事件。
- mouseenter :在鼠标光标从元素外部首次移动到元素范围之内时触发。这个事件不冒泡,而且在光标移动到后代元素上不会触发。
- mouseleave :在位于元素上方的鼠标光标移动到元素范围之外时触发。这个事件不冒泡,而且在光标移动到后代元素上不会触发。
- mouseover :在鼠标指针位于一个元素外部,然后用户将其首次移入另一个元素边界之内时触发。不能通过键盘触发这个事件。
- 鼠标事件都是在浏览器视口中的特定位置上发生的。这个位置信息保存在事件对象的 clientX 和clientY 属性中。所有浏览器都支持这两个属性,它们的值表示事件发生时鼠标指针在视口中的水平和垂直坐标。
- 通过客户区坐标能够知道鼠标是在视口中什么位置发生的,而页面坐标通过事件对象的 pageX 和pageY 属性,能告诉你事件是在页面中的什么位置发生的。换句话说,这两个属性表示鼠标光标在页面中的位置,因此坐标是从页面本身而非视口的左边和顶边计算的。
- 在页面没有滚动的情况下, pageX 和 pageY 的值与 clientX 和 clientY 的值相等。
- 鼠标事件发生时,不仅会有相对于浏览器窗口的位置,还有一个相对于整个电脑屏幕的位置。而通过 screenX 和 screenY 属性就可以确定鼠标事件发生时鼠标指针相对于整个屏幕的坐标信息。
- 键盘与文本事件:
- keydown :当用户按下键盘上的任意键时触发,而且如果按住不放的话,会重复触发此事件。
- keyup :当用户释放键盘上的键时触发。
- 键码: 在发生 keydown 和 keyup 事件时, event 对象的 keyCode 属性中会包含一个代码,与键盘上一个特定的键对应。对数字字母字符键, keyCode 属性的值与 ASCII 码中对应小写字母或数字的编码相同。
- DOMContentLoaded 事件则在形成完整的 DOM树之后就会触发,不理会图像、JavaScript 文件、CSS 文件或其他资源是否已经下载完毕。与 load 事件不同,DOMContentLoaded 支持在页面下载的早期添加事件处理程序,这也就意味着用户能够尽早地与页面进行交互。
- 在 JavaScript 中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能。导致这一问题的原因是多方面的。首先,每个函数都是对象,都会占用内存;内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 DOM访问次数,会延迟整个页面的交互就绪时间。
对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如, click 事件会一直冒泡到document 层次。也就是说,我们可以为整个页面指定一个 onclick 事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。
在不需要的时候移除事件处理程序,也是解决这个问题的一种方案。内存中留有那些过时不用的“空事件处理程序”(dangling event handler),也是造成 Web 应用程序内存与性能问题的主要原因。 - HTML5 为文本字段新增了 pattern 属性。这个属性的值是一个正则表达式,用于匹配文本框中的值。例如,如果只想允许在文本字段中输入数值,可以像下面的代码一样应用约束:
<input type="text" pattern="\d+" name="count">
- 要使用 <canvas> 元素,必须先设置其 width 和 height 属性,指定可以绘图的区域大小。
2D 上下文的坐标开始于 <canvas> 元素的左上角,原点坐标是(0,0)。所有坐标值都基于这个原点计算,x 值越大表示越靠右,y 值越大表示越靠下。 - 大多数 2D 上下文操作都会细分为填充和描边两个操作,而操作的结果取决于两个属性: fillStyle 和 strokeStyle。
var drawing = document.getElementById("drawing");
//确定浏览器支持<canvas>元素
if (drawing.getContext){
var context = drawing.getContext("2d");
context.strokeStyle = "red";
context.fillStyle = "#0000ff";
}
- 矩形是唯一一种可以直接在 2D 上下文中绘制的形状。与矩形有关的方法包括 fillRect() 、strokeRect() 和 clearRect() 。这三个方法都能接收 4 个参数:矩形的 x 坐标、矩形的 y 坐标、矩形宽度和矩形高度。这些参数的单位都是像素。
var drawing = document.getElementById("drawing");
//确定浏览器支持<canvas>元素
if (drawing.getContext){
var context = drawing.getContext("2d");
/*
* 根据 Mozilla 的文档
* http://developer.mozilla.org/en/docs/Canvas_tutorial:Basic_usage
*/
// 绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 绘制半透明的蓝色矩形
context.fillStyle = "rgba(0,0,255,0.5)";
context.fillRect(30, 30, 50, 50);
}
- 要绘制路径,首先必须调用 beginPath() 方法,表示要开始绘制新路径。
- arc(x, y, radius, startAngle, endAngle, counterclockwise) :以 (x,y) 为圆心绘制一条弧线,弧线半径为 radius ,起始和结束角度(用弧度表示)分别为 startAngle 和endAngle 。最后一个参数表示 startAngle 和 endAngle 是否按逆时针方向计算,值为 false表示按顺时针方向计算。
- arcTo(x1, y1, x2, y2, radius) :从上一点开始绘制一条弧线,到 (x2,y2) 为止,并且以给定的半径 radius 穿过 (x1,y1) 。
- bezierCurveTo(c1x, c1y, c2x, c2y, x, y) :从上一点开始绘制一条曲线,到 (x,y) 为止,并且以 (c1x,c1y) 和 (c2x,c2y) 为控制点。
- lineTo(x, y) :从上一点开始绘制一条直线,到 (x,y) 为止。
- moveTo(x, y) :将绘图游标移动到 (x,y) ,不画线。
// 绘制一个不带数字的时钟表盘
var drawing = document.getElementById("drawing");
//确定浏览器支持<canvas>元素
if (drawing.getContext){
var context = drawing.getContext("2d");
// 开始路径
context.beginPath();
// 绘制外圆
context.arc(100, 100, 99, 0, 2 * Math.PI, false);
// 绘制内圆
context.moveTo(194, 100);
context.arc(100, 100, 94, 0, 2 * Math.PI, false);
// 绘制分针
context.moveTo(100, 100);
context.lineTo(100, 15);
// 绘制时针
context.moveTo(100, 100);
context.lineTo(35, 100);
// 描边路径
context.stroke();
}
在绘制内圆之前,必须把路径移动到内圆上的某一点,以避免
绘制出多余的线条。最后一步是调用 stroke() 方法,这样才能把图形绘制
到画布上。
- 绘制文本主要有两个方法: fillText() 和strokeText() 。两个方法都可以接收 4 个参数:要绘制的文本字符串、x 坐标、y 坐标和可选的最大像素宽度。
- 如果你知道将来还要返回某组属性与变换的组合,可以调用 save() 方法。调用这个方法后,当时的所有设置都会进入一个栈结构,得以妥善保管。然后可以对上下文进行其他修改。等想要回到之前保存的设置时,可以调用 restore() 方法,在保存设置的栈结构中向前返回一级,恢复之前的状态。
context.fillStyle = "#ff0000";
context.save();
context.fillStyle = "#00ff00";
context.translate(100, 100);
context.save();
context.fillStyle = "#0000ff";
context.fillRect(0, 0, 100, 200); //从点(100,100)开始绘制蓝色矩形
context.restore();
context.fillRect(10, 10, 100, 200); //从点(110,110)开始绘制绿色矩形
context.restore();
context.fillRect(0, 0, 100, 200); //从点(0,0)开始绘制红色矩形
- 如果你想把一幅图像绘制到画布上,可以使用 drawImage()方法。根据期望的最终结果不同,调用这个方法时,可以使用三种不同的参数组合。最简单的调用方式是传入一个 HTML <img> 元素,以及绘制该图像的起点的 x 和 y 坐标。
var image = document.images[0];
context.drawImage(image, 10, 10);
如果你想改变绘制后图像的大小,可以再多传入两个参数,分别表示目标宽度和目标高度。通过这种方式来缩放图像并不影响上下文的变换矩阵。
context.drawImage(image, 50, 10, 20, 30);
- 要创建一个新的线性渐变,可以调用createLinearGradient()方法。这个方法接收 4 个参数:起点的 x 坐标、起点的 y 坐标、终点的 x 坐标、终点的 y 坐标。调用这个方法后,它就会创建一个指定大小的渐变,并返回CanvasGradient 对象的实例。
创建了渐变对象后,下一步就是使用 addColorStop() 方法来指定色标。这个方法接收两个参数:色标位置和 CSS 颜色值。色标位置是一个 0(开始的颜色)到 1(结束的颜色)之间的数字。
var gradient = context.createLinearGradient(30, 30, 70, 70);
gradient.addColorStop(0, "white");
gradient.addColorStop(1, "black");
可以把 fillStyle 或 strokeStyle 设置为这个对象,从而使用渐变来绘制形状或描边:
//绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
//绘制渐变矩形
context.fillStyle = gradient;
context.fillRect(30, 30, 50, 50);
- 要创建径向渐变(或放射渐变),可以使用 createRadialGradient() 方法。
-
跨文档消息传送(cross-document messaging),有时候简称为XDM,指的是在来自不同域的页面间传递消息。
XDM 的核心是 postMessage() 方法。
对于 XDM 而言,“另一个地方”指的是包含在当前页面中的 <iframe> 元素,或者由当前页面弹出的窗口。
postMessage() 方法接收两个参数:一条消息和一个表示消息接收方来自哪个域的字符串。第二个参数对保障安全通信非常重要,可以防止浏览器把消息发送到不安全的地方。
//注意:所有支持 XDM 的浏览器也支持 iframe 的 contentWindow 属性
var iframeWindow = document.getElementById("myframe").contentWindow;
iframeWindow.postMessage("A secret", "http://www.wrox.com");
- 媒体元素
<!-- 嵌入视频 -->
<video src="conference.mpg" id="myVideo">Video player not available.</video>
<!-- 嵌入音频 -->
<audio src="song.mp3" id="myAudio">Audio player not available.</audio>
使用 <audio> 和 <video> 元素的 play() 和 pause() 方法,可以手工控制媒体文件的播放。
- try-catch语句
try{
// 可能会导致错误的代码
} catch(error){
// 在错误发生时怎么处理
}
只要代码中包含 finally 子句,那么无论 try 还是 catch 语句块中的 return 语句都将被忽略。因此,在使用 finally 子句之前,一定要非常清楚你想让代码怎么样。
function testFinally(){
try {
return 2;
} catch (error){
return 1;
} finally {
return 0;
}
}
- 在找不到对象的情况下,会发生 ReferenceError (这种情况下,会直接导致人所共知的 "object expected" 浏览器错误)。通常,在访问不存在的变量时,就会发生这种错误。
- TypeError 类型在 JavaScript 中会经常用到,在变量中保存着意外的类型时,或者在访问不存在的方法时,都会导致这种错误。错误的原因虽然多种多样,但归根结底还是由于在执行特定于类型的操作时,变量的类型并不符合要求所致。
var obj = x; //在 x 并未声明的情况下抛出 ReferenceError
var o = new 10; //抛出 TypeError
alert("name" in true); //抛出 TypeError
Function.prototype.toString.call("name"); //抛出 TypeError
- JavaScript 字符串与 JSON 字符串的最大区别在于,JSON 字符串必须使用双引号(单引号会导致语法错误)。
JSON 中的对象要求给属性加引号。
// JS对象字面量
var object = {
"name": "Nicholas",
"age": 29
};
//JSON 表示上述对象的方式
{
"name": "Nicholas",
"age": 29
}
与 JavaScript 的对象字面量相比,JSON 对象有两个地方不一样。首先,没有声明变量(JSON 中没有变量的概念)。其次,没有末尾的分号(因为这不是 JavaScript 语句,所以不需要分号)。再说一遍,对象的属性必须加双引号,这在 JSON 中是必需的。
- JSON 对象有两个方法: stringify() 和 parse() 。在最简单的情况下,这两个方法分别用于把JavaScript 对象序列化为 JSON 字符串和把 JSON 字符串解析为原生 JavaScript 值。
在序列化 JavaScript 对象时,所有函数及原型成员都会被有意忽略,不体现在结果中。此外,值为undefined 的任何属性也都会被跳过。结果中最终都是值为有效 JSON 数据类型的实例属性。
JSON.stringify() 除了要序列化的 JavaScript 对象外,还可以接收另外两个参数,这两个参数用于指定以不同的方式序列化 JavaScript 对象。第一个参数是个过滤器,可以是一个数组,也可以是一个函数;第二个参数是一个选项,表示是否在 JSON 字符串中保留缩进。
有时候, JSON.stringify() 还是不能满足对某些对象进行自定义序列化的需求。在这些情况下,可以给对象定义 toJSON() 方法,返回其自身的 JSON 数据格式。 -
Ajax,是对 Asynchronous JavaScript + XML 的简写。这一技术
能够向服务器请求额外的数据而无须卸载页面,会带来更好的用户体验。
Ajax 技术的核心是 XMLHttpRequest 对象(简称 XHR) - 在使用 XHR 对象时,要调用的第一个方法是 open() ,它接受 3 个参数:要发送的请求的类型( "get" 、 "post" 等)、请求的 URL 和表示是否异步发送请求的布尔值。
URL是相对于执行代码的当前页面的(当然也可以使用绝对路径);调用 open() 方法并不会真正发送请求,而只是启动一个请求以备发送。
xhr.open("get", "example.txt", false);
xhr.send(null);
这里的 send() 方法接收一个参数,即要作为请求主体发送的数据。如果不需要通过请求主体发送数据,则必须传入 null ,因为这个参数对有些浏览器来说是必需的。调用 send() 之后,请求就会被分派到服务器。
一般来说,可以将 HTTP状态代码为 200 作为成功的标志。
状态代码为 304 表示请求的资源并没有被修改,可以直接使用浏览器中缓存的版本。
- GET是最常见的请求类型,最常用于向服务器查询某些信息。必要时,可以将查询字符串参数追加到 URL 的末尾,以便将信息发送给服务器。
- 使用频率仅次于 GET 的是 POST 请求,通常用于向服务器发送应该被保存的数据。 POST 请求应该把数据作为请求的主体提交,而 GET 请求传统上不是这样。
与 GET 请求相比, POST 请求消耗的资源会更多一些。从性能角度来看,以发送相同的数据计, GET 请求的速度最多可达到 POST 请求的两倍。
- 通过 XHR 实现 Ajax 通信的一个主要限制,来源于跨域安全策略。默认情况下,XHR 对象只能访问与包含它的页面位于同一个域中的资源。这种安全策略可以预防某些恶意行为。但是,实现合理的跨域请求对开发某些浏览器应用程序也是至关重要的。
CORS(Cross-Origin Resource Sharing,跨源资源共享)是 W3C 的一个工作草案,定义了在必须访问跨源资源时,浏览器与服务器应该如何沟通。CORS 背后的基本思想,就是使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。
比如一个简单的使用 GET 或 POST 发送的请求,它没有自定义的头部,而主体内容是 text/plain。在发送该请求时,需要给它附加一个额外的 Origin 头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给予响应。请求和响应都不包含 cookie 信息。 - 要请求位于另一个域中的资源,使用标准的 XHR对象并在 open() 方法中传入绝对 URL即可。
跨域 XHR 对象也有一些限制,但为了安全这些限制是必需的。以下就是这些限制。
- 不能使用 setRequestHeader() 设置自定义头部。
- 不能发送和接收 cookie。
- 调用 getAllResponseHeaders() 方法总会返回空字符串。
由于无论同源请求还是跨源请求都使用相同的接口,因此对于本地资源,最好使用相对 URL,在访问远程资源时再使用绝对 URL。这样做能消除歧义,避免出现限制访问头部或本地 cookie 信息等问题。
- JSONP是 JSON with padding(填充式 JSON 或参数式 JSON)的简写,是应用 JSON 的一种新方法。JSONP 看起来与 JSON 差不多,只不过是被包含在函数调用中的 JSON。例如:
callback({ "name": "Nicholas" });
- Ajax 是一种从页面向服务器请求数据的技术,而 Comet 则是一种服务器向页面推送数据的技术。Comet 能够让信息近乎实时地被推送到页面上,非常适合处理体育比赛的分数和股票报价。
有两种实现 Comet 的方式:长轮询和流。长轮询是传统轮询(也称为短轮询)的一个翻版,即浏览器定时向服务器发送请求,看有没有更新的数据。流在页面的整个生命周期内只使用一个 HTTP 连接。具体来说,就是浏览器向服务器发送一个请求,而服务器保持连接打开,然后周
期性地向浏览器发送数据。 - SSE(Server-Sent Events,服务器发送事件)是围绕只读 Comet 交互推出的 API 或者模式。SSE API用于创建到服务器的单向连接,服务器通过这个连接可以发送任意数量的数据。
- Web Sockets的目标是在一个单独的持久连接上提供全双工、双向通信。
- 对于未被授权系统有权访问某个资源的情况,我们称之为 CSRF(Cross-Site Request Forgery,跨站点请求伪造)。
确保通过 XHR 访问的 URL 安全,通行的做法就是验证发送请求者是否有权限访问相应的资源。有下列几种方式可供选择。
- 要求以 SSL 连接来访问可以通过 XHR 请求的资源。
- 要求每一次请求都要附带经过相应算法计算得到的验证码。
请注意,下列措施对防范 CSRF 攻击不起作用。 - 要求发送 POST 而不是 GET 请求——很容易改变。
- 检查来源 URL 以确定是否可信——来源记录很容易伪造。
- 基于 cookie 信息进行验证——同样很容易伪造。
- HTML5 的应用缓存(application cache),或者简称为 appcache,是专门为开发离线 Web 应用而设计的。Appcache 就是从浏览器的缓存中分出来的一块缓存区。要想在这个缓存中保存数据,可以使用一个描述文件(manifest file),列出要下载和缓存的资源。
要将描述文件与页面关联起来,可以在 <html> 中的 manifest 属性中指定这个文件的路径,例如:
<html manifest="/offline.manifest">
- HTTP Cookie,通常直接叫做 cookie,最初是在客户端用于存储会话信息的。
cookie 在性质上是绑定在特定的域名下的。当设定了一个 cookie 后,再给创建它的域名发送请求时,都会包含这个 cookie。这个限制确保了储存在 cookie 中的信息只能让批准的接受者访问,而无法被其他域访问。
由于 cookie 是存在客户端计算机上的,还加入了一些限制确保 cookie 不会被恶意使用,同时不会占据太多磁盘空间。每个域的 cookie 总数是有限的,不过浏览器之间各有不同。
当超过单个域名限制之后还要再设置 cookie,浏览器就会清除以前设置的 cookie。I - 存储在 localStorage 中的数据和存储在 globalStorage 中的数据一样,都遵循相同的规则:数据保留到通过 JavaScript 删除或者是用户清除浏览器缓存。
Web Storage 定义了两种用于存储数据的对象: sessionStorage 和 localStorage 。前者严格用于在一个浏览器会话中存储数据,因为数据在浏览器关闭后会立即删除;后者用于跨会话持久化数据并遵循跨域安全策略。 - 可维护的代码:可理解性、直观性、可适应性、可扩展性、可调试性。
- 变量名应为名词。
- 函数名应该以动词开始。
- 变量和函数都应使用合乎逻辑的名字,不要担心长度。长度问题可以通过后处理和压缩来缓解。
当定义了一个变量后,它应该被初始化为一个值,来暗示它将来应该如何应用。
- 有关脚本性能的注意事项:
- 原生方法较快;
- Switch语句较快 —— 如果有一系列复杂的 if-else 语句,可以转换成单个 switch 语句则可以得到更快的代码。
- 位运算符较快 —— 当进行数学运算的时候,位运算操作要比任何布尔运算或者算数运算快。
- 优化DOM交互
- 最小化现场更新:
一旦你需要访问的 DOM 部分是已经显示的页面的一部分,那么你就是在进行一个现场更新。之所以叫现场更新,是因为需要立即(现场)对页面对用户的显示进行更新。每一个更改,不管是插入单个字符,还是移除整个片段,都有一个性能惩罚,因为浏览器要重新计算无数尺寸以进行更新。现场更新进行得越多,代码完成执行所花的时间就越长;完成一个操作所需的现场更新越少,代码就越快。
- 当谈及 JavaScript 文件压缩,其实在讨论两个东西:代码长度和配重(Wire weight)。代码长度指的是浏览器所需解析的字节数,配重指的是实际从服务器传送到浏览器的字节数。
网友评论