JS学习笔记
一、作用域相关
基本变量的值一般都是存在栈内存中而对象类型的变量的值存储在堆内存中,即值存储和引用存储。栈内存存储对应空间地址,主要是基本的数据类型: Undefined、Null、Boolean、Number、String集中变量存在栈空间中。
执行上下文有以下几种:
SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。- 全局代码:代码默认运行的环境,最先会进入到全局执行上下文(web浏览器中被认为是window对象)中。
- 函数代码:在函数的局部环境中运行的代码。特别的,Eval作用域是在Eval()函数中运行的代码
作用域相关注意事项如下:
- JS只有全局作用域和函数作用域。不用任何操作符定义的变量为全局作用域,在块语句内部定义的变量会保留在它们已经存在的作用域内。
- 函数申明先于变量申明——ECO创建阶段决定的。
// 不用任何操作符定义的变量为全局作用域
function func(){
// b没使用var声明为全局变量。a为局部变量且绑定在func函数下,外部访问不到。
var a = b = 20;// 是指 var a = b; b = 20
console.log(a);// 20
console.log(b);// 20
function f2() {
console.log(a);// 20
console.log(b);// 20
}
f2();
// 在块语句内部定义的变量会保留在它们已经存在的作用域内
if(true) {
var word = 'hello';
console.log(word);// hello
}
console.log(word);// hello
}
func();
console.log(a);// error
console.log(b);// 20
var存在变量提升,let不存在变量提升。
"声明提前“这步操作是在JavaScript引擎的"预编译"时进行的,是在代码开始运行之前。函数声明分为声明式和引用式。声明式会提前和覆盖(涉及变量提升),引用式不会提前和覆盖。
var scope = "glocal";
function f(){
// 隐士操作
// var scope;// 作用域覆盖了全局的scope
console.log(scope);// 输出"undefined"而不是"global"
var scope = "local";// 变量在这里赋初始值,但变量已经提升到函数起始位置
console.log(scope);// 输出"local"
}
1、ECS
1.1 ECO
执行上下文为一个对象,包含VO,作用域链、this:
executionContextObj = {
// 函数中的arguments对象、参数、内部的变量以及函数声明
variableObject: {...},
// VO以及所有父执行上下文中的VO
scopeChain: {...},
this: {...}
}
ECO建立过程如下:
找到当前上下文调用函数的代码
创建阶段:函数被调用但还未执行函数中的代码。
2.1 创建变量对象VO(该上下文中的所有变量和函数全都保存在这个对象中。):
a. 创建arguments,检查当前上下文的参数,建立该对象下的属性和属性值
b. 扫描上下文的函数申明:每扫描到一个函数就会在VO里面用函数名创建一个属性(为一个指针),指向该函数在内存中的地址。如果函数名在VO中已经存在,对应的属性值会被新的引用覆盖。
c. 扫描上下文的变量申明:每扫描到一个变量就会用变量名作为属性名,其值初始化为undefined。如果该变量名在VO中已经存在,则直接跳过继续扫描。
2.2 初始化作用域链
2.3 确定上下文中this的指向执行阶段:执行函数体中的代码,给VO中的变量赋值以及代码执行。
作用域链ScopeChain:
每一段JS代码(全局或函数或eval)都有一个与之关联的作用域链(是一个对象链表),记录了这段代码"作用域中"的变量,内容是从栈顶到栈底的对象的顺序链接:作用域链的头部始终是当前执行代码所在上下文的变量对象,末端始终是全局上下文的变量对象。
当JS需要“变量解析”变量x的值的时会从链中的第一个对象开始查找:如果这个对象有一个名为x属性,则会直接使用这个属性的值;如果第一个对象中不存在,则会继续寻找下一个对象,依次类推。如果作用域链上没有任何一个对象含有属性x则抛出错误(ReferenceError)异常。图片讲解见https://images2015.cnblogs.com/blog/775815/201610/775815-20161026115117875-1515546527.png。
1.2 ECS
任务都为同步任务的情况下某一时间只能执行一个任务,一段代码所有的EC都会被推入ECS中等待被执行。
执行上下文栈ECS的过程如下:
- 全局上下文压入栈顶
- 执行某一函数就为其创建一个EC,并压入栈顶
- 栈顶的函数执行完之后,函数的EC就会从ECS中弹出,并且变量对象(VO)随之销毁
- 所有函数执行完之后ECS中只剩下全局上下文,在应用关闭时销毁
2、闭包
闭包:可访问另一个函数作用域变量的函数。即闭包是个函数,这个函数能够访问其他函数的作用域中的变量。
闭包会携带外部函数的作用域,因此会比其他函数占用更多内容。过度使用闭包,会导致内存占用过多。
很多人会搞不懂匿名函数与闭包的关系,两者没有任何关系。实际上闭包是站在作用域的角度上来定义的:内部函数访问到外部函数作用域的变量,所以内部函数就是一个闭包函数。虽然定义很简单但是有很多坑点如this指向、变量的作用域,稍微不注意可能就造成内存泄露。
function func() {
var num = 10;
return function() {
return num;
}
}
var fn = func();
console.log(fn()); // 10
作用:
- 希望一个变量长期驻扎在内存中。
- 避免全局变量的污染。
- 私有成员的存在(或者产生后块级作用域)。
2.1 坑点
坑点1:this指向问题
var object = {
name: "object",
getName: function() {
return function() {
console.info(this.name);
}
}
}
// 因闭包函数是在window作用域下执行的,也就是说this指向windows
object.getName()();// underfined
坑点2:引用变量可能发生变化
// 原始代码
function outer(){
var result = [];
for (var i = 0; i<10; i++){
result.[i] = function () {
console.info(i);
}
}
return result;
}
// 看样子result每个闭包函数对打印对应数字1,2,3,4,...,10
// 因为每个闭包函数访问变量i是outer执行环境下的变量i,随着循环的结束i已经变成10
// 所以结果打印10, 10, ..., 10
// 修复代码
function outer(){
var result = [];
for (var i = 0; i<10; i++){
result.[i] = function (num) {
return function() {
// 数组有10个函数对象,每个对象的执行环境下的number都不一样
console.info(num);
}
}(i)
}
return result;
}
坑点3:内存泄露问题
function showId(){
var el = document.getElementById("app")
el.onclick = function(){
aler(el.id);// 这样会导致闭包引用外层的el,当执行完showId后el无法释放
}
}
// 改成下面
function showId(){
var el = document.getElementById("app");
var id = el.id;
el.onclick = function(){
aler(id);// 这样会导致闭包引用外层的el,当执行完showId后el无法释放
}
el = null;// 主动释放el
}
2.2 技巧
技巧1: 用闭包解决递归调用问题
function factorial(num){
if(num<= 1) {
return 1;
} else {
// arguments.callee指向当前执行函数,但是在严格模式下不能使用该属性也会报错
// 所以可以考虑借助闭包来实现
return num * arguments.callee(num-1);
}
}
var anotherFactorial = factorial;
anotherFactorial(4);
// 使用闭包实现递归
// 实际上起作用的是闭包函数f,而不是外面的函数newFactorial
function newFactorial = (function f(num){
if(num<1) {
return 1
} else {
return num* f(num-1)
}
})
技巧2:用闭包模仿块级作用域
ES6没出来之前,用var定义变量存在变量提升问题。当然现在大多用es6的let和const定义。eg:
for(var i=0; i<10; i++){
console.info(i)
}
alert(i);// 变量提升,弹出10
// 为了避免i的提升可以这样做
(function () {
for(var i=0; i<10; i++){
console.info(i)
}
})()
alert(i);// underfined。因为i随着闭包函数的退出,执行环境销毁,变量回收
二、对象相关
JavaScript 中,万物皆对象(拥有属性和方法)。对象可以分为普通对象和函数对象,Object 、Function 是 JS 自带的函数对象。对象是JavaScript的一个基本数据类型,是一种复合值:它将很多值(原始值或者其他对象)聚合在一起,可通过名字访问这些值。即属性的无序集合。
1、对象创建
最基本创建对象方式如下:
// 其原型对象继承于为Object
var person = {
name:'Bill',
age:59,
sayName:function(){
console.info(this.name);
}
}
// 其原型对象继承于为Object
var person = new Object();
person.name = "Bill";
person.age = 59;
person.sayName = function(){
console.info(this.name);
};
// 使用工厂模式创建对象
// 以上两种方式用同个接口创建很多对象则会产生大量重复代码。如有100个对象,就要输入100次相同的代码
// 把上述创建对象的过程封装在函数体内,通过函数的调用就可以直接生成对象,这样就可避免上述问题
function newPerson(){
var person = {
name:'Bill',
age:59,
sayName:function(){
console.info(this.name);
}
}
return person;
}
它的缺点:在newPerson函数中返回的是一个对象,无法判断返回的对象究竟是一个什么样的类型。
1.1 构造模式
不用new时函数内的this指向的是window。用new后JS引擎会在函数进行两步隐士操作:
第一步
var this = Object.create(Peson.prototype);
来隐士改变函数内this的含义,现在函数内的this是原型为Person.prototype且 构造函数为Person的对象。其实此过程将想要的对象基本创造成功了,只是差些属性而已(这些就需要自定义了)。因此可看出构造函数创建对象的最根本原理是借用Object.create()方法来实现的,只不过被封装了。Object.create(原型) 创建一个继承该原型的实例对象。
- 若原型参数为Object.prototype,则创建的原型为Object.prototype(和new Object()是一样)。
- 若传参为空或null,则创建的对象是没有原型的。会导致该对象是无法用document.write()打印会报错,因document.write()打印的原理是调用该类型的prototype.toString()方法。
第二步在创建的对象设置完所需要的属性后,隐士的将创建的对象this返回(return this)。
将上述创建对象的属性和方法设置给函数自身后,该函数定义就是新的派生类型。因此改变如下:
// 使用构造模式创建对象
// 按照惯例,构造函数要应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头
function Person(name){
// 隐士操作:创建this且其中存在一个属性__proto__,该属性存储了Person.prototype
// var this = Object.create(Person.prototype);
// 设置对象属性
this.name = name || "Bill";
this.age = 59,
this.sayName = function(){
console.info(this.name);
};
// 隐士操作
// return this;
}
// 测试代码
var person = new Person('Bill');
它的缺点:每个方法都要在每个实例上重新创建一遍(方法指的是在对象里面定义的函数)。如果方法的数量很多就会占用很多不必要的内存。
1.2 原型模式
原型的定义:原型是对象的一个内置属性且原型也是对象。它定义了构造函数创建出的对象的公共祖先,即构造函数创建出的对象可以继承该原型的属性和方法。
对象都有个属性对象--原型对象(__proto__),原型对象指向构造函数的原型属性对象。对象能访问构造函数定义的原型属性中的内容靠的就是这个原型对象。
Function还都有个属性对象--原型属性(prototype),原型属性是构造函数用于定义各种属性的对象。这个属性对象有一个属性constructor,其存储的是构造函数自身。
针对__proto__属性有以下说明:
- __proto__存放:在上述构造函数模式的对象创建过程中在用new创建一个对象时,内部会隐士自动创建一个this的对象,进过一系列处理后再隐士将this对象返回。而__proto__就对于隐士创建的this对象中。
- __proto__查看:直接通过new操作符创建的对象访问__proto__属性即可。
- __proto__修改:__proto__可以理解为静态属性,因此修改原型对象内的属性会影响所有的类型创建的对象的其属性的值。
__proto__是原型链的关键,prototype是原型链的组成。Object是作为一个构造函数,Object.__proto__指向Function.prototype,Object.prototype为null而null没有原型。下面是一张图示:
-----------function Person(){} --- Person.prototype | | | __proto__ | | | * |------------function Object(){} --- [Object.prototype] ——*__proto__=null | * | | | __proto__ | | |-----------function Function(){} --- Function.prototype | * __proto__ | | | |----------------------------------------------
将一类对象的共有属性提取出来放到该类对象的原型中形成共享(静态)属性,从而不需要每次用new操作符时都重新定义一遍这些共有属性。因此改变如下:
// 使用原型模式创建对象
// 使用对象原型创建对象的方式可以让所有对象实例共享它所包含的属性和方法
function Person(name){}
// 原型设置
Person.prototype = {
constructor:Person,
name:"Bill",
age:59,
sayName:function(){
console.info(this.name);
return this;// 实现类似jquery中的链式调用return this,此时this指向该原型对象
}
}
// 测试代码
var person1 = new Person();
var person2 = new Person();
person1.__proto__.name = "Test";
console.info(person1.name); // Test
console.info(person2.name); // Test
它的缺点:一个实例改变原型属性的值则其他实例都会受影响。因原型属性数共享的。
1.3 组合模式
结合构造模式和原型模式,可形成规范:构造函数用于定义实例属性,原型对象用于定义共享属性。
// 使用组合模式创建对象
function Person(name){
// 隐士操作:创建this且其中存在一个属性__proto__,该属性存储了Person.prototype
// var this = Object.create(Person.prototype);
// 当为实例对象添加一个属性时,这个属性会屏蔽原型对象中保存的同名属性。
this.name = name || "Bill";// name普通对象的属性而非原型对象的属性
// 隐士操作
// return this;
}
// 原型设置
Person.prototype = {
constructor:Person,
name:"Bill",
age:59,
sayName:function(){
console.info(this.name);
return this;// 实现类似jquery中的链式调用return this,此时this指向该原型对象
}
}
// 测试代码
var person1 = new Person();
var person2 = new Person();
person1.name = "Test";// 这是设置的自身属性而非原型属性
console.info(person1.name); // "Test" --来自自身属性
console.info(person2.name); // “Bill” --来自原型属性
2、对象继承
先定义父类:
// 定义父类
function Animal(name){
this.name = name || "Animal";
this.run = function(){
console.info(this.name + '正在跑!');
}
}
Animal.prototype.eat = function(food){
console.info(this.name + '正在吃' + food);
};
2.1 构造继承
使用父类构造函数来增强子类构造函数(等于直接是复制父类的实例属性给子类):
构造继承也叫做 “伪造对象” 或 “经典继承” 。基本思想是:在子类构造函数的内部调用父类构造函数。函数只是在特定环境中执行代码的对象,因此可通过使用call()和apply()方法在新创建的对象上执行构造函数。
// 定义子类
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
// 测试代码
var cat = new Cat();
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
console.log(cat.name);
console.log(cat.run());
// console.log(cat.eat('fish'));// 错误的,没有该eat属性,因为不能继承父类原型属性
这种方法的继承的缺点:只能继承父类的自身属性,不能继承父类的原型属性(实例并不是父类的实例,只是子类的实例 )。
2.2 原型继承
使用父类实例对象设置为子类原型对象:
// 定义子类
function Cat(){}
Cat.prototype = new Animal();// 这是重点
// 此时Cat.prototype.constructor为Animal
// 因此原型继承是需要修复构造函数指向的
Cat.prototype.constructor = Cat;
// 测试代码
var cat = new Cat();
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
console.log(cat.name);
console.log(cat.run());
console.log(cat.eat('fish'));
这种方法的继承的缺点:父类的自身属性和原型属性是所有实例共享的,这是不能容忍的!!!
2.3 组合继承
使用父类构造函数来增强子类构造函数且使用父类实例对象设置为子类原型对象:
// 定义子类
function Cat(name){
// 设置父类的自身属性为子类自身属性(覆盖了后续子类原型属性中相同的属性)
Animal.call(this);
this.name = name || 'Tom';
}
Cat.prototype = new Animal();
// 此时Cat.prototype.constructor为Animal
// 因此组合继承是需要修复构造函数指向的
Cat.prototype.constructor = Cat;
// 测试代码
var cat = new Cat();
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
console.log(cat.name);
console.log(cat.run());
console.log(cat.eat('fish'));
缺点就是调用了两次父类构造函数,生成了两份实例,但是已经解决了属性继承和作用域的问题。
2.4 圣杯模式
每次继承的都是新创建的F构造函数实例,相互之间不会影响。其实此处针对F形成了闭包,Child引用了F导致F不会销毁。
// 正常形式
function extend(Child, Parent){
// 借用F这个中间量来继承,而不是直接共享原型
var F = function (){}
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.prototype.parent = Parent;// 记录真正继承的是谁
}
// 闭包形式
var extend = (function(){
var F = function (){};
return function (Child, Parent) {
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.prototype.parent = Parent;// 记录真正继承的是谁
}
})();
3、对象克隆
对象克隆分为浅克隆和深克隆,针对引用值直接赋值的克隆操作为浅克隆,克隆的和被克隆的对象在克隆操作完成后,指向同一个地址引用,改变其中一个(注意:此处的改变为增加或删除对象的属性,而不是为该对象重新赋值一个对象),另一个也会改变,而深克隆则不会产生此现象。
// 对象的深度克隆(array/object/...)
function clone(obj){
var str1,str2 = Object.prototype.toString.call(obj) === '[object Array]' ? [] : {};
if(typeof obj !== 'object') {
return;
}else if(window.JSON) {
str1 = JSON.stringify(obj);
str2 = JSON.parse(str1);
}else {
for(var prop in obj) {
str2[prop]=typeof obj[prop]==='object' ? clone(obj[prop]) : obj[prop];
}
}
return retObj;
}
4、对象篡改
4.1 属性修改
针对于对象属性的增删改查如下:
var person = new Person('Bill');
// 使用属性
// 对于属性查询过程如下:
// 首先看构造函数中是否有要查询的属性,若有则直接返回
// 若没有则看其原型有没有要查询的属性,若没有则再看原型的原型上是否有要查询的属性
// 以此顺序在原型链上查询,若一直到原型链顶端后仍没有要查询的属性则返回undefined
console.info(person.name);
// 枚举对象所有属性
// in操作符用来枚举对象person上所有属性
for(var prop in person){
console.info((typeof person[prop]) + “类型:” + prop);
}
// 判断对象自身属性
// hasOwnProperty方法用来判断对象person的自身属性中是否含有属性name
// hasOwnProperty方法还可判断该属性是从原型继承来的还是自身的
person.hasOwnProperty('name');
// isPrototypeOf用来判断指定对象object1是否存在于另一个对象object2的原型链中
// 添加对象自身属性
person.age = 59;
console.info(person.age);// 59
// 删除对象自身属性
delete person.name;
delete person.sayName;
我们可以添加、删除或修改其他的属性或方法。但这也会导致一些问题,如多人开发时某些属性被人为修改而造成工程上的麻烦。这就促使了防篡改对象的诞生。
4.2 属性冻结
防篡改对象有三个级别:不可拓展对象、密封对象、冻结对象。一旦对象被设置放篡改对象则不能撤销,所以需要慎重考虑。
4.2.1 不可拓展对象属性
可以将普通对象设置为不可拓展对象,来使新添加的属性无效:
// 防拓展对象
Object.preventExtensions(person);// 将对象设置为防拓展对象
person.smallName = 'Gates';
console.info(person.smallName);// undefined,说明不能添加新的属性
//虽然防拓展对象不能添加属性,但是可以删除属性
delete person.name;
console.info(person.name);//undefined,已经删除成功
// 检测是否为可拓展对象
console.info(Object.isExtensible(person));// false
4.2.2 密封对象属性
不可拓展对象可以防止新添加属性,却不能阻止他人删除属性(当然也不能阻止修改)。密封对象就是在不可拓展对象的基础上添加一条规则不能删除属性:
// 密封对象
Object.seal(person);// 将对象设置为密封对象
delete person.name;
console.info(person.name);// Bill,说明不能删除已有属性
// 虽然不能删除,但是可以修改
person.name = "Gates";
console.info(person.name);// Gates,说明修改成功
// 检测是否为密封对象
console.info(Object.isSealed(person));// true
4.2.3 冻结对象属性
密封对象虽然防止了删除,但是无法阻止修改属性。冻结对象不能添加、删除和修改属性:
// 冻结对象
Object.freeze(person);
// 检测是否为冻结对象
console.info(Object.isFrozen(man));// true
4.3 对象属性
JS中对象内置多个属性(Configurable、Enumerable、Writable、Value、Get、Set)来控制属性的行为。一般的属性都是有4种设置属性:可配置属性configurable、可枚举属性enumerable、可读属性writable、数值属性value。但由于对象中存在一类特别的属性存取器属性,所以对于存取器属性的值实际上是不同的,它有自己特别的属性特性:可配置configurable、可枚举enumerable、读取get、写入set。为了实现这一对象属性的描述,JS中定义了一个属性描述符对象,并且可以通过相关函数获取:
- Object.getOwnPropertyDescriptor()方法获取对象自身属性的描述符。
- Object.getPrototypeOf()方法来获取对象继承属性的描述符。
- Object.defineProperty方法来进行对象内容的进行相关的编辑。它将返回一个定义的对象。
当在定义一个对元素的属性的时候,要注意上面前两个方法在某些情况之下是会报错误的,情况如下:
- 对象是不可扩展的,所以我们只能对原有的自有属性进行编辑,但不可以添加新的属性。
- 如果属性不可配置的,数据属性和存储器属性不能互相转换。
- 如果数据属性是不可配置的,则不可以修改它的可配置性和可枚举性,其可写属性不可改false为true,但是可以修改true为false。
- 如果存储器属性是不可配置的,则不可以修改它的setter和getter属性。
5、对象附录
5.1、原始类型
JS有五个原始类型:null、undefined、boolean、number、string。其中boolean、number、string是分别拥有自己的包装类,而undefined和null是没有自己的包装类的。原始类型不是对象从而无法拥有自己自定义的属性,但包装类可拥有自己的属性。
每次包装类包装完一次完整语句后就会被销毁(即解释一条语句,用包装类包装一次,然后销毁)。这会导致其和正常对象的一些不同之处:
var str = 'abcd'; // 后台会转换为str = (var str1= new String('abcd'));的包装 // 销毁包装对象str1 str.length = 4; console.log(str.length); // 值是4 // 为基本类型添加属性问题分析 var str = 'abcd'; // 后台会转换为str = (var str1= new String('abcd'));的包装 // 销毁包装对象str1 str.size = 4; // 后台会转换为str = (var str1= new String('abcd'));的包装并进行str1.size = 4; // 销毁包装对象str1,但是str为基本类型从而不能拥有自定义属性size console.info(str.size); // 值是undefiend,因为添加的size是在str1上而非str上
三、事件相关
以下讲解均基于该代码:
<body>
<div id="parent">
父元素
<div id="child">
子元素
</div>
</div>
<script type="text/javascript">
// 通过"addEventListener"方法,采用事件冒泡方式给dom元素注册click事件
var parent = document.getElementById("parent");
var child = document.getElementById("child");
document.body.addEventListener("click",function(e){
console.info("click-body");
},false);
parent.addEventListener("click",function(e){
console.info("click-parent");
},false);
child.addEventListener("click",function(e){
console.info("click-child");
},false);
</script>
</body>
DOM事件流(event flow)有两种:事件冒泡、事件捕获。无论是事件捕获还是事件冒泡,它们都有一个共同的行为事件传播。它就像一跟引线,只有通过引线才能将绑在引线上的鞭炮(事件监听器)引爆。
DOM标准事件流的触发的先后顺序为:先捕获再冒泡。即当触发DOM事件时,会先进行事件捕获,捕获到事件源之后通过事件传播进行事件冒泡。不同的浏览器对此有着不同的实现:IE10及以下不支持捕获型事件,所以就少了一个事件捕获阶段,IE11、Chrome 、Firefox、Safari等浏览器则同时存在。
说到事件冒泡与捕获就不得不提一下两个用于事件绑定的方法addEventListener、attachEvent。当然还有其它的事件绑定的方式,这里不做介绍:
addEventListener(event, listener, useCapture)
addEventListener在IE11、Chrome、Firefox、Safari等浏览器都得到支持。
- event:事件名称,如click,不带on
- listener:事件监听函数
- useCapture:是否采用事件捕获进行事件捕捉。默认为false(即采用事件冒泡方式)。
attachEvent(event,listener)
attachEvent主要用于IE浏览器,并且仅在IE10及以下才支持。
- event:事件名称,如click,不带on
- listener:事件监听函数
1、事件冒泡
事件冒泡(dubbed bubbling):顺序是由内到外进行事件传播,直到根节点。
// 上述过程,点击子元素依次会输出:click-child、click-parent、click-body
// 如果点击子元素不想触发父元素的事件怎么办
// 在原始代码新增停止事件传播--event.stopPropagation()
child.addEventListener("click",function(e){
console.info("click-child");
e.stopPropagation();
},false);
// 点击子元素只会输出:click-child
2、事件捕获
事件捕获(event capturing):顺序是由外到内进行事件传播,直到叶子节点。如点击了子元素,如果父元素通过事件捕获方式注册了对应的事件的话,会先触发父元素绑定的事件。
// 在原始代码新增事件捕获事件代码
parent.addEventListener("click",function(e){
console.info("click-parent--事件捕获");
},true);
// 点击子元素依次会输出:click-parent--事件捕获、click-child、click-parent、click-body
3、事件委托
事件委托还有一个名字叫事件代理,一般用于动态生成的元素。事件委托是利用事件冒泡原理来实现的,只指定一个事件处理程序,就可以管理某一类型的所有事件。
有三个同事预计会在周一收到快递。为签收快递有两种办法:一是三个人在公司门口等快递;二是委托给前台代为签收。现实当中大都采用委托的方案(公司也不会容忍那么多员工站在门口就为了等快递)。前台MM收到快递后,她会判断收件人是谁,然后按照收件人的要求签收,甚至代为付款。这种方案还有一个优势,那就是即使公司里来了新员工(不管多少),前台MM也会在收到寄给新员工的快递后核实并代为签收。这里有2层意思的:
- 现在委托前台的同事是可以代为签收的,即程序中的现有的dom节点是有事件的
- 新员工也是可以被前台MM代为签收的,即程序中新添加的dom节点也是有事件的
一般来说,DOM需要有事件处理程序,直接给它设事件处理程序就好了!如果是很多的DOM(比如有100个li)需要添加事件处理呢?每个li都有相同的click点击事件,可能会用for循环的方法来遍历所有的li,然后给它们添加事件,那这么做会存在什么影响呢?
每个函数都是一个对象,对象越多内存占用率就越大,自然性能就越差了。如上面的100个li就要占用100个内存空间。
如果用事件委托,那么就可以只对它的父级这一个对象进行操作,这样需要一个内存空间就够了。
在JS中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能,因为需要不断的与dom节点进行交互。访问dom的次数越多,引起浏览器重绘与重排的次数也就越多,就会延长整个页面的交互就绪时间。这就是为什么性能优化的主要思想之一就是减少DOM操作的原因。
如果要用事件委托,就会将所有的操作放到js程序里面,与dom的操作就只需要交互一次,这样就能大大的减少与dom的交互次数,提高性能
适合用事件委托的事件:click、keydown、keyup、keypress。不适合的主要分为两大类:一类是没有冒泡机制的,如focus、blur;另一类是每次都要计算它的位置而非常不好把控的如mousemove。
通过原生的js去实现的,下面举一个简单的栗子:
// 通过js实现通过parent元素给child元素注册click事件
// 当多个元素有相同的事件,将事件绑定在父元素。这里可以用多个子元素判断
parent.onclick = function(e){
if(e.target.id == "child"){
console.info("您点击了child元素")
}
}
// 这里虽然没有直接给child元素注册click事件,可是点击child元素时却弹出了提示信息
四、其他相关
1、console方法
- clear():清空控制台信息。
- info():info()前有个蓝色的图标。
- warn():warn()前有个黄色的图标。
- error():error()前有个红色的图标。
- trace():不仅会打印函数调用栈信息,同时也会显示函数调用中各参数的值。
- dir():使输出内容格式化更易读,而且也会输出一个对象的全部属性和方法。
2、this
- 函数预编译过程this指向window
- 全局作用域里this指向window
- 函数调用中func()里面的this指向调用者obj。可以这样理解:正常使用条件下谁调用func则this就指向谁,而非call/apply使用方式。
3、操作符
===和==
三等号本质上是内存比较。当进行三等号比较时,如果类型不同直接就是false,都则进行内存比较。
三等号时,如果两个值都是null,或是undefined,那么相等,否则不相等。
三等号时,如果两个都是字符串且每个位置的字符都一样,那么相等,否则不相等。
双等号本质上是数值比较。当进行双等号比较时,当进行双等号比较时先检查两个操作数数据类型,如果相同则进行三等号比较。如果不同则进行一次类型转换, 转换成相同类型后再进行比较。
双等号时,如果一个是null且另一个是undefined,那么相等,否则不相等。
双等号时,如果两个都是字符串且每个位置的字符都一样,那么相等,否则不相等。
instanceof和typeof
instanceof用来判断实例对象person是否为Person构造函数创建的。
console.info(person instanceof Person); // true
typeof用来获取一个变量或者表达式的类型,typeof一般只能返回如下几个结果:undefined、boolean、number、string、function、object(NULL,数组,对象)。
console.info(typeof(person));// "object"
call和apply
每个函数都包含两个非继承而来的方法:call()方法和apply()方法。这两个方法都是在特定的作用域中调用函数,等于设置函数体内this对象的值,以扩充函数赖以运行的作用域。
一般来说this总是指向调用某个方法的对象,但是使用call()和apply()方法时就会改变this的指向。
call()方法使用示例:
用法:call是将参数一个个传进来:
call([obj[,arg1 [,arg2 [,...,argn]]]]);
。说明:如果没有提供obj参数,那么Global对象被用于obj。
window.color = 'white'; document.color = 'black'; var object = {color: 'blue' }; function changeColor(){ console.info(this.color); } changeColor.call(); //white,this指向window changeColor.call(this); //white,this指向window changeColor.call(window); //white,this指向window changeColor.call(document); //black,this指向document changeColor.call(object); //blue,this指向object
apply()方法使用示例:
用法:apply是将所有参数存进一个数组中,然后将该数组传进来:
apply([obj [,argArray]]);
。说明:如果argArray不是一个有效数组或不是arguments对象,那么将导致一个TypeError,如果没有提obj和argArray任何一个参数,那么Global对象将用作obj。
window.color = 'white'; document.color = 'black'; var s1 = {color: 'blue' }; function changeColor(){ console.log(this.color); } changeColor.apply(); //white,this指向window changeColor.apply(this); //white,this指向window changeColor.apply(window); //white,this指向window changeColor.apply(document); //black,this指向document changeColor.apply(object); //blue,this指向object
window.onload和$(document).ready()
window.onload是在页面中包含图片在内的素有元素全部加载完成再执行。
$(document).ready()是DOM文档树加载完成之后执行,不包含图片以及其他媒体文件。
因此$(document).ready()快于window.onload执行。
Cookie和Storage
Cookie用于客户端与服务端通信,也具有本地存储的功能
大小:Cookie容量为4K,因为用于客户端与服务端通信因此所有http都携带,如果太大会降低效率。sessionStorage、localStorage大小为5M。
时间:Cookie会在浏览器关闭时删除,除非主动设置删除时间。sessionStorage在浏览器关闭时删除;localStorage一直都在直到用户主动删除或清除浏览器缓存。
附录
