一、事件冒泡和事件捕获

1.事件冒泡

事件冒泡:事件从事件源(事件目标)开始,往上冒泡直接到页面的根元素

特点:先触发子元素的事件,再触发父级元素的事件

// 一般的绑定事件的方式,采用的都是事件冒泡,如element.onclick = function(){}
document.querySelector('#div1').onclick = function(){
    console.log('div1');
}
document.querySelector('#div2').onclick = function(){
    console.log('div2');
}
document.querySelector('#div3').onclick = function(){
    console.log('div3');
}

2.1.阻止事件冒泡

/**
 *调用事件对象的stopPropagation()来阻止事件传播
 *event表示事件对象,以下需使用event.target来表示事件源
 */

document.querySelector('#div1').addEventListener('click',function(){
    console.log('div1');
});
document.querySelector('#div2').addEventListener('click',function(event){
    event.stopPropagation();
    console.log('div2');
});
document.querySelector('#div3').addEventListener('click',function(event){
        console.log('div3');
});

2.事件捕获

事件捕获:事件从页面的根元素开始,往下直到事件源 (事件目标)

特点:先触发根级元素的事件,再触发子级元素的事件

3.事件流

当一个 HTML 元素产生事件时,该事件会在当前元素与根元素之间按特定的顺序传播,所有经过的节点都会收到该事件并执行,这个传播的过程就是 DOM 事件流

3.1.控制事件流

// 通过addEventListener(事件名,回调函数,事件流方式)绑定事件
document.querySelector('#div1').addEventListener('click',function(){
    console.log('div1');
},true); // 第三个参数为false表示事件冒泡,true表示事件捕获,默认值为false
document.querySelector('#div2').addEventListener('click',function(){
    console.log('div2');
},true);
document.querySelector('#div3').addEventListener('click',function(){
    console.log('div3');
},true);

二、事件代理/事件委托

事件代理/事件委托:

利用事件冒泡机制,通过给父元素绑定事件,从而实现对所有子元素的事件管理,无需为每个子元素绑定事件

优点:

  1. 减少事件注册,降低内存占用
  2. 新增元素时实现动态绑定事件
<script>
window.onload = function(){
    document.querySelector('ul').onclick = function(event){
        console.log(e.target.innerHTML); // 此处的target为触发事件的目标对象,即子元素li,而非父元素ul
    }
}
function add(){
   var li = document.createElement('li');
   li.innerText = 'li6';
   document.querySelector('ul').appendChild(li);
}
</script>

<body>
    <button onclick="add()">添加li</button>
    <ul>
        <li>li1</li>
        <li>li2</li>
        <li>li3</li>
        <li>li4</li>
        <li>li5</li>
    </ul>
</body>

三、阻止事件的默认行为

事件的默认行为:

某些事件触发时默认会执行一些行为,如:右键点击时默认会弹出菜单、点击超链接时会跳转

<script>
function print(event){
    event.preventDefault(); // 调用preventDefault()来阻止事件的默认行为
}
</script>

<body>
    <button oncontextmenu="print(event)">右键点击</button> <br>
</body>

四、Object 常用方法

1.Object 常用方法

Object.defineProperty();
Object.defineProperties();
Object.keys(object);  //获取对象的所有属性名的集合,返回一个数组
Object.values(object);  //获取对象的所有属性值的集合

2.defineProperty 的用法

  • 为对象添加一个属性,添加后不允许再修改
  • Object.defineProperty(object,propertyName,descriptor) 参数分别表示:对象、属性、属性描述符
Object.defineProperty(user,'sex',{
    value:'male', //属性的默认值
    writable:false, //是否允许修改该属性,默认为false
    configurable:false, //是否允许删除该属性,默认为false
    enumerable:true, //是否允许遍历该属性,默认为false
});

2.1.ES6 新增 defineProperty 用法

Object.defineProperty(object,propertyName,descriptor)

  • 作用:拦截对属性的赋值和取值的操作,称为数据劫持!!!
var temp;
Object.defineProperty(user,'name',{
    // 赋值
    set:function(value){ // 参数value为赋值时传入的值
        console.log('执行了set函数,进行赋值的操作');
        temp = value.toUpperCase(); // 做额外的处理,进行扩展操作
    },
    // 取值
    get:function(){
        console.log('执行了get函数,进行取值的操作');
        return temp;
    }
});

// 赋值
user.name = 'zhangsan';

// 取值
console.log(user.name);

3.defineProperties 的用法

  • 为对象定义多个属性
  • Object.defineProperty(object,propertyName,descriptor) 参数分别表示:对象、对象、属性描述符
Object.defineProperties(user,{
    address:{
        value:'四川',
        writable:true,  //是否允许修改该属性,默认为false
        configurable:false  //是否允许删除该属性,默认为false
    },
    height:{
        value:180.5,
        configurable:true  //是否允许删除该属性,默认为false
    }
});

六、基本数据类型和引用数据类型

1.数据类型的分类

  1. 基本数据类型 string、number、boolean、null、undefined
  2. 引用数据类型 Object、Array、Date、User、Student...等

声明变量时分配至不同的内存

//基本数据类型
var a = 5;
var b = a; //将a的值复制一份给b
b = 8;
console.log(a);
console.log(b);

//引用数据类型
var nums = [1,2,3,4];
var nums2 = nums;  //将nums的地址赋给nums2,即让nums2和nums指向同一块内存空间
nums2[0] = 8;
console.log(nums);
console.log(nums2);

2.JavaScript 中内存的两个区域(类型)

1050487-20170627224452680-492511428.png

栈内存

  • 基本数据类型的变量以及引用数据类型的引用,会存储在栈中
  • 存取速度较快,空间较小

堆内存

  • 引用数据类型的值,会存储在堆中
  • 存取速度较慢,空间较大

3.将数据类型作为参数

//基本数据类型传参
function f1(a){
    a = 8;
}
var x = 5;
f1(x);  //传的是数据,按值传递
console.log(x);

//引用数据类型传参
function f2(arr){
    arr[0] = 666;
}
var nums = [1,2,3];
f2(nums); // 传的是地址,按引用传递
console.log(nums);

六、instanceof 关键字

1.常规判断数据类型

var a = 'tom';

function Student(name,age){
    this.name = name;
    this.age = age;
}

var stu1 = new Student('tom',20);

//使用typeof
console.log(typeof a);
console.log(typeof 12);
console.log(typeof false);
console.log(typeof [1,2,3]);
console.log(typeof new Date());
console.log(typeof stu1);
console.log(typeof null);

2.instanceof 判断

instanceof 变量是否为某种类型,返回 boolean

语法:数据 instanceof 类型

var b = [1,2,3];
var c = new Date();
console.log(b instanceof Array);
console.log(c instanceof Student);
console.log(stu1 instanceof Date);

if(b instanceof Date){
    console.log(c.getFullYear());  //getFullYear()表示返回一个表示年份的 4 位数字。
}

七、闭包

1.什么是闭包

  • 概念 1:JS 中特有现象,在一个函数内部又定义了一个函数,这个定义在内部的函数,就是闭包
  • 概念 2:闭包就是能够读取其他函数内部变量的函数
  • 概念 3:闭包是在某个作用域内定义的函数,该函数可以访问这个作用域内的所有变量

从作用上来说,闭包就是将函数内部和函数外部连接起来的一座桥梁

function f1(){
    var n = 99; // 局部变量
    function f2(){ // 这里的f2就是闭包
        console.log(n); // 在内部函数f2中可以访问到外部函数f1中的局部变量n
    }
    return f2; // 返回函数f2
}

var fn = f1();
fn(); // 此时可以读取到函数f1内部的变量 */

2.闭包的用途

用途 1:在函数的外部,可以读取到函数内部的变量

用途 2:让变量的值始终保存在内存中(不会被垃圾回收器回收)

function f1(){
    var n = 99;
    function f2(){
        console.log(n++);
    }
    return f2;
}

var fn = f1();
fn();
fn(); // 函数执行两次都输出结果,说明f1函数中局部变量n一直在保持在内存中

使用闭包的注意事项:由于闭包会使函数中的变量都被保存到内存中,内存消耗会比较大

八、原型 prototype

1.什么是 prototype

  • 在构造函数中有一个属性叫 prototype
  • prototype 是一个对象属性,其属性值为对象,称为原型对象
  • 可以通过 prototype 来添加新的属性和方法,并且新的属性和方法是该构造函数所创建对象所共享的
// 定义一个构造函数
function Student(name,age){
    this.name = name; // 实例属性,每个实例都具有一个独立的该属性
    this.age = age;
    this.show = function(){
        console.log('我是一个学生,姓名:'+this.name+',年龄:'+this.age);
    }
}

Student.prototype.sayHello = function(){
    console.log('你好');
}
Student.prototype.sayGoodbye = function(){
    console.log('再见');
}
Student.prototype.address = '北京海淀';

2.prototype 添加属性或方法

通过为 prototype 添加新的属性或方法,此时所有该构造函数创建的对象都会具有这些属性和方法

语法:

  • 构造函数.prototype.属性名=属性值
  • 构造函数.prototype.方法名 = function(){ }
// 创建一个对象
var stu1 = new Student('tom',18);
stu1.show();
stu1.sayHello();
stu1.sayGoodbye();
console.log(stu1.address);

console.log('--------------------------------------');

var stu2 = new Student('jack',20);
stu2.show();
stu2.sayHello();
stu2.sayGoodbye();
console.log(stu2.address);

/**
  * stu1.sayHello 和 stu2.sayHello分别使用不同的内存空间,都要占用资源
  * 但其内容是完全相同的,为了节省内存空间,希望能够共享
  */
console.log(stu1 === stu2); // false
console.log(stu1.sayHello === stu2.sayHello); // true

console.log(stu1 === stu2); // false
console.log(stu1.sayHello === stu2.sayHello); // true

3.所有构造函数都具有一个 prototype 属性

//例
console.log(Student.prototype);
console.log(Date.prototype);
console.log(Array.prototype);

4.访问对象属性的查找顺序

1.首先在当前对象中查找对应的实例属性

2.如果没有,就会到该对象关联的构造函数的 prototype 属性中查找,即到原型对象中查找

Student.prototype.sex = '男'; // 共享的属性
stu1.sex = '女'; // 为stu1添加了一个实例属性sex
console.log(stu1.sex);

5.proto 与 prototype 的关系

  • prototype 是一个隐藏属性,于是为每个对象提供了一个 proto 的属性
  • 对象的proto与创建它的构造函数的 prototype 本质上是同一个东西
function Student(name){
    this.name = name;
    this.show = function(){
        console.log('我是一个学生,姓名:'+name);
    }
}

var stu1 = new Student('tom');
var stu2 = new Student('jack');
// console.log(stu1.name);
// console.log(stu1.__proto__);
console.log(stu1);
console.log(stu1.__proto__ === Student.prototype);
console.log(stu2.__proto__);

总结:

  1. Student.prototype 是构造函数 Student 的原型属性,是对象 stu1 的原型对象
  2. proto是对象的属性,是站在对象的角度,来讨论其原型对象
  3. prototype 是构造函数的属性,是站在构造函数的角度,来讨论其原型属性
  4. 由于 proto 是非标准属性,因此一般不建议使用

九、原型链

对象原型链结构.png

  • 任何对象都有其原型对象,原型对象也有原型对象,对象的原型对象一直往上找,找到 null 为止
  • 在这一过程中,有一个 Object 类型的,有很多方法,它就是 Object.prototype,位于顶层
var nums = new Array(12,4,34,7);
console.log(nums);

nums.reverse();
nums.push(56);
nums.toString();

function User(name){
    this.name = name;
}
User.prototype.msg = 'welcome';

var u = new User('tom');
console.log(u);
u.toString();

var o1 = new Object();
console.log(o1);
// o1 ----> Object.prototype ----> null

console.log(Object === Object.prototype);

十、对象的类型

1.对象的类型是什么

var obj1 = new Student();
var obj2 = new Teacher();

console.log(obj1 instanceof Student);
console.log(obj2 instanceof Teacher);
console.log(typeof(obj1)); // object
console.log(typeof(obj2)); // object

总结:构造函数的名称就是对象的类型名

2.constructor 属性

  • 每个对象都有一个 constructor 属性
  • 对象的 contructor 属性是其原型对象提供的、
  • contructor 属性描述的就是其构造函数(构造器)
function User(name){
    this.name = name;
}
User.prototype.msg = 'welcome';

var u = new User('tom');
console.log(u);
console.log(u.constructor); // 指向了创建该对象的构造函数
console.log(u.constructor === User); // true
// console.log(u.msg);

// 通过调用构造函数的name属性,可以获取函数名
console.log(u.constructor.name);

console.log(obj1.constructor.name);
console.log(obj2.constructor.name);

十一、arguments 关键字

  • 在每个函数的内部都有 arguments,是一个(伪)数组,包含函数的所有实参
  • arguments 是一个伪数组,既其结构上类似 Array,但并不是真正的数组,不能调用 Array 的方法
  • 可以使用 arguments 接收函数的所有实参
//例一
function show(num1,num2,num3){
    console.log(num1,num2,num3);
    // arguments.push(666); // arguments是一个伪数组,既其结构上类似Array,但并不是真正的数组,不能调用Array的方法
    console.log(arguments); // 可以使用arguments接收函数的所有实参
    console.log(arguments.length);
    console.log(arguments.constructor.name); // Object

    var nums = new Array(12,4,2);
    nums.push(666);
    console.log(nums.constructor.name);
}

show(12,21,43);
show(12,21,43,11,22,33);
//例二
function sum(){
    let result = 0;
    for(let i=0;i<arguments.length;i++){
        result += arguments[i];
    }
    return result;
}

let result = sum(1,2,3,4,5,6,7,8);
console.log(result);

console.log(sum(12,5));
console.log(sum(1,2,3));
console.log(sum(1,2,3,4));

十二、call 和 apply

在对象中,一个对象包含了属性和方法

1.永久赋值

// 定义了一个对象,包含属性和方法
var stu = {
  //属性
    name:'tom',
    age:20,
  //方法
    study(){
        console.log('正在学习。。。');
    }
}
stu.study(); // 调用对象的方法

// 定义一个函数
function run(){
    console.log('正在跑步。。。。',this);
}
run();

stu.run = run;
stu.run();

2.临时调用

// 定义了一个对象,包含属性和方法
var stu = {
  //属性
    name:'tom',
    age:20,
  //方法
    study(){
        console.log('正在学习。。。');
    }
}
stu.study(); // 调用对象的方法

// 定义一个函数
function run(){
    console.log('正在跑步。。。。',this);
}
run();

run.call(stu);  // stu对象临时调用run()函数
run.apply(stu); // stu对象临时调用run()函数

3.this 的转变

// 定义了一个对象,包含属性和方法
var stu = {
  //属性
    name:'tom',
    age:20,
  //方法
    study(){
        console.log('正在学习。。。');
    }
}
stu.study(); // 调用对象的方法

// 定义一个函数
function run(){
    console.log('正在跑步。。。。',this);
}
run();

run.call(stu);  // stu对象临时调用run()函数
run.apply(stu); // stu对象临时调用run()函数

var date = new Date();
window.run();  // 此时的this指向window
run.call(stu); // 此时的this表示调用的对象stu,称为对象冒充
run.apply(date);

4.call 和 apply 的区别

  • 作用:以某个对象的身份来调用另一个对象的方法

  • 区别:传参的方式不同

    • 第 1 个参数是相同的,都表示由该对象来调用执行
      • 后面的参数不同

call 是逐个传参,后面参数可以有多个,逗号隔开

apply 是以数组形式传参,后面参数只能有一个,会自动拆分为多个元素传入

function show(num1,num2,num3){
    console.log(num1+num2+num3);
}

show.call(stu,1,2,3);
show.apply(stu,[1,2,3]);

使用 call 来实现继承,即通过对象冒充来实现继承

function Person(name,age){
    this.name = name;
    this.age = age;
    this.run = function(){
        console.log('正在跑步。。。');
    }
}

function Student(name,age,sex){
    Person.call(this,name,age); // 继承
    this.sex = sex;
    this.play = function(){
        console.log('正在玩游戏。。。。');
    }
}

var stu = new Student('tom',20,'male');
console.log(stu.name,stu.age,stu.sex);
stu.run();
stu.play();