《Javascript 高级程序设计(第三版)》通读后的总结笔记(六)

184 阅读22分钟

6 JS深入

6.1 重新认识JS

  • JS是一种JIT编译语言,也叫“即时编译”,是”动态编译“的一种,当某段代码即将第一次被执行时进行编译。解释一行执行一行,相较于其他语言较慢。

    java、c#等语言是“编译执行”,一次性把代码编译成可执行代码,再一行一行执行,速度较快。

  • 具有灵活性,动态特性,可以随时随意给对象增加属性和方法

  • 函数是JS中的一等公民

  • 一般在宿主环境下运行,如浏览器(除node.js)

  • JS运行前会进行“预解析”,先全局再函数内部

6.2 面对对象编程

6.2.1 编程思想

  • 面向过程:所有的事情都亲力亲为,注重过程
  • 面向对象:提出需求 - 找对象 - 对象解决,注重结果
  • JS不是一门面向对象的语言,而是基于对象,将执行者转变成指挥者,用JS来模拟面向对象的语言,它不是面向过程的替代,而是面向过程的封装
  • 面向对象の特性
    • 封装:即包装,把一些重用的内容进行打包,在需要时直接使用
    • 继承:类与类之间的关系,JS中没有类的概念,但有构造函数的概念,基于原型,是可以有继承的
    • 多态(抽象性):同一个行为,针对不同的对象,产生不同的效果
  • 面向对象编程(OOP),是一种编程开发思想,是过程式代码的一种高度封装,目的在于提高代码的开发效率和可维护性,比起传统过程式编程,更适合多人合作大型软件项目
  • 面向对象的抽象程度比函数要高,因为一个类/模板(Class)既包含数据,又包含操作数据的方法

6.2.2 体会面向对象

//面向过程的方式

// 1 记录学生的成绩
var stu1 = {name: 'zs', subject: '语文', score: 90};
var stu2 = {name: 'ls', subject: '语文', score: 80};
// 2 打印学生的成绩
console.log(stu1.name, stu1.subject, stu1.score);
console.log(stu2.name, stu2.subject, stu2.score);
// 面向对象的方式

/*-----------------------------------------
	首选思考的不是程序执行流程,而是把`Student`视为对象,并拥有`name`和`score`两个属性;
	而打印成绩,首先必须创建出这个`Student`对应的对象,再给对象发一个`printScore`消息,让对象把自己的数据打印出来
-----------------------------------------*/

// 1 抽象数据行为变成模板 (Class)
function Student(name, subject, score) {
    this.name = name;
    this.subject = subject;
    this.score = score;

    this.printScore = function () {
        console.log(this.name, this.subject, this.score);
    }
}
// 2 根据模板创建具体实例对象 (Instance)
var stuA = new Student('zs', '语文', 90);
var stuB = new Student('ls', '语文', 80);
// 3 实例对象具有自己的具体行为 (指挥Instance得到结果)
stuA.printScore();
stuB.printScore();

/*-----------------------------------------
	在此例中,我们处理的多个对象都具有一个共同点,即他们都是学生,所以把他们抽象归成 类/模板(Class),再根据每一个具体的学生去创建具体的实例对象(Instance),最后再单独赋予给他们各自的具体行为
-----------------------------------------*/
  1. 在JS中创建对象的模板是构造函数,而在其他语言中创建对象的模板是类
  2. 创建实例对象(Instance)时,需要使用new操作符

6.2.3 创建对象的方法

// 1 new Object()创建
// var person = {};		也可以省略new Object() 缩写成{}
var person = new Object();
person.name = "Nico";
person.age = 21;
person.sayHi = function(){
    console.log("hello, I am" + this.name);
}

// 2 对象字面量创建
var person = {
	name: "Nico",
	age: 21;
    sayHi: function(){
    	console.log("hello, I am" + this.name);
	}
}

// 3 工厂函数
/*---------------------------------------
	当要创建多个实例对象时 就要重复写很多份对象 代码太冗余 不方便 使用我们使用工厂函数 它用于批量创建多个对象 解决代码重复问题
----------------------------------------*/

function createPerson (name, age) {
    return {
        name: name,
        age: age,
        sayHi: function () {
      		console.log("hello, I am" + this.name);
}}}
	// 再生成实例对象
var p1 = createPerson("Nico", 21);
var p2 = createPerson("Mike", 18);


/*-------------------------------------
工厂函数虽解决了批量创建对象的问题 但存在缺点:它无法确定真实的对象类型
-------------------------------------*/
console.log(typeof p1);   // Object
console.log(p1 instanceof createPerson);    // false

构造函数

  • 构造函数是专门用来创建对象的函数

  • 一个构造函数我们也可以称为一个类

  • 通过一个构造函数创建的对象,我们称该对象时这个构造函数的实例

  • 通过同一个构造函数创建的对象,我们称为一类对象

  • 构造函数就是一个普通的函数,只是他的调用方式不同

    • 如果直接调用,它就是一个普通函数
    • 如果使用new来调用,则它就是一个构造函数
// 4 构造函数

/*------------------------------------
1 这里引入更优解 即更优雅的工厂函数 即构造对象的函数
2 构造函数比工厂函数创建更方便  
    2.1 会在内存中创建一个空对象 (无需内部创建新对象)
    2.2 直接将属性和方法赋给了this 让this指向刚刚创建好的对象
    2.3 执行构造函数中的代码
    2.4 没有return语句 无需返回对象(会自动返回)
3 构造函数一般用首字母大写 普通函数用小写 便于区分
4 同时解决了工厂函数不能识别对象具体类型的缺陷
------------------------------------*/

function Person (name, age) {
    this.name = name,
    this.age = age,
    this.sayHi = function () {
        console.log("hello, I am" + this.name);
}}
	// 生成实例对象必须使用new操作符
var p1 = new Person("Nico", 21);
var p2 = new Person("Mike", 18);

console.log(p1 instanceof Person);	// true

/*---------------------------------------
	每一个实例对象中的 __proto__ 中有一个constructor属性,该属性指向创建该实例的构造函数,它最初是用来标识对象类型的,它虽然可以用于检测对象的类型,但是生产中还是使用instanceof操作符更可靠。

console.log(p1.constructo);		// 返回它的对象构造器
console.log(p1.constructor === Person);	// true
--------------------------------------*/
/*-----------------------------------------
	用上面的方法去构造函数虽然方便,但当多个对象去调用这个构造函数时,会产生存储多个方法的内存区域,会造成极大的内存浪费。
	
p1.sayHi();
p2.sayhi();
console.log(p1.sayHi === p2.sayHi); // false 
// 说明两者不是指向同一方法 而是创建了两块内存区来存储

	所以我们把需要共享的函数定义到构造函数的外部,但若有多个需要共享的函数,可能会造成全局命名空间冲突的问题,因此有可以把多个函数放到一个对象中来保存来避免问题。
----------------------------------------*/
function sayHi() {
    console.log("hello, I am" + this.name);
}
//或
var fns = {
    sayHi: function () {
        console.log("hello, I am" + this.name);
	},
    sayAge: function () {
        console.log("I am" + this.age);
	}
}
// 这样基本上解决了构造函数的内存浪费问题 但代码看起来会有些怪 所以引入原型

原型

​ 通过构造函数创建的对象,解析器都会默认在函数中添加一个prototype属性对象,即每一个构造函数都有一个prototype属性,称为原型/原型对象。原型中的成员,都会被构造函数的实例间接继承,故可以把所有对象实例需要共享的属性和方法直接定义在prototype对象上。

// 5 原型
/*-----------------------------------------
	
-------------------------------------------*/
function Person (name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.type = "student";
Person.prototype.sayHi = function () {
	console.log("hello, I am" + this.name);
}

var p1 = new Person("Nico", 21);
var p2 = new Person("Mike", 18);

console.log(p1.sayHi === p2.sayHi);		//true

构造函数、实例、原型三者之间的关系

console.log(p1.__proto__ === Person.prototype);	//true
console.log(p1.constructor === Person);			//true
  • prototype 属性是一个对象,所有实例属性和方法都指向它,它们共用同一个内存地址,标准属性,给程序员使用
  • __proto__属性是非标准的属性,指向构造函数的原型,供浏览器使用
  • constructor是构建器,指向构建函数

实例对象访问原型对象中的成员的搜索原则:原型链

  1. 搜索首先从对象实例本身开始
  2. 若在实例中找到了指定属性,则返回该属性的值
  3. 若未找到,则继续搜索指针指向的原型对象,在原型对象中查找指定属性
  4. 若在原型对象中找到了指定属性,则返回该属性的值
  5. 若一直到原型链末端还未找到,则返回 undefined
  6. Object的原型的原型为null
// 6 最优的原型
/*-----------------------------------
	为减少不必要的每次都输入一遍Person.prototype,常见做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象
----------------------------------*/
function Person (name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype = {
    constructor: Person, //避免原型对象丢失了constructor成员
	type: "student",
	sayHi: function () {
		console.log("hello, I am" + this.name);
	}
}

什么数据需要写在原型中

  1. 数据共享是原型的作用之一,故需要共享的数据就可以写在原型中
  2. 不需要共享的数据写在构造函数中

说明

  • 任何函数都有prototype属性对象,包括原生对象的原型,如Date.prototype,因此可以利用此特性来扩展原生对象
  • 最好不要让实例之间互相共享数组或对象成员,因为一旦修改会导致数据的走向很不明确,而且难以维护
  • 原型对象的使用建议
    • 私有成员(如非函数成员)放到构造函数中

    • 共享成员(一般为函数)放到原型对象中

    • 如果重置了 prototype 记得修正 constructor 的指向

原型的作用

  1. 数据共享(为了节省内存空间)
  2. 继承

6.3 继承

  • 继承是指一个对象直接使用另一对象的属性和方法
  • 一般会创建子类和父类,子类可使用父类的所有功能,且对这些功能进行扩展
  • 继承有两种方式:接口继承和实现继承,ECMAScript只支持实现继承,主要是依靠原型链来实现

6.3.1 对象拷贝

对象拷贝:将对象的成员复制一份给需要继承的对象,但并非真正的继承,真正的继承是类与类的关系

// 案例1 一般的对象拷贝
var obj1 = {
	name: 'zs',
	age: 18,
	sex: '男'
}
var obj2 = {};

// 封装函数 - 把o1的成员复制给o2
function copy(o1, o2) {
	for (var key in o1) {
		o2[key] = o1[key];
	}
}
copy(obj1, obj2);
console.dir(obj2);	// obj2 拥有了obj1的name age sex属性

obj1.name = 'xx';	// 修改obj1的成员
console.dir(obj2);	// 不会影响obj2

那么要如何实现 完全的拷贝 呢?

这里涉及到递归的知识 去查看 ----> 递归 (按住Ctrl点击)

// 案例2

// 创建父对象
var father = {
    name: '王健林',
    money: 10000000,
    cars: ['BMW', 'Tesla'],
    hobby: function () {
    	console.log('Play golf');
	}
}
// 创建需要继承的子对象
var son = {
	name: '王思聪'
}

// 对象拷贝函数 (使用for...in...循环 并封装到函数中)
function extend(parent, child) {
    for (var key in parent) {
    	if (child[key]) {	// 若有相同属性则跳过
    		continue;
    	}
    	child[key] = parent[key];
    }
}

extend(father, son);

// 此时son就也有了father的cars money hobby 但name依然自己保留

/*--------------------------------------
	对象拷贝存在问题:
    如果继承过来的成员是引用类型的话(即对象属性 对象里包含另一个对象),那么这个引用类型的成员在父对象和子对象之间是共享的,也就是说修改了之后,父子对象都会受到影响。
    这个问题可以通过函数"递归"来解决。
---------------------------------------*/

6.3.2 原型式继承

// 借用构造函数的原型对象实现继承

// 创建父构造函数
function Parent(name){
    this.name = name;
    this.showName = function(){
    	console.log(this.name);
    }
}
// 设置父构造器的原型对象
Parent.prototype.showAge = function(){
	console.log(this.age);
}

// 创建子构造函数
function Child(){
    
}
// 设置子构造函数的原型对象实现继承
Child.prototype = Parent.prototype;

var child = new Child();
console.dir(child);	//child只在原型上继承了showAge方法


/*--------------------------------------
问题:
	父构造函数的原型对象和子构造函数的原型对象上的成员有共享问题,包含引用类型的属性值会共享,且只能继承父构造函数的原型对象上的成员, 不能继承父构造函数的实例对象的成员
--------------------------------------*/

6.3.3 原型链继承

原型链:每个对象都有原型对象,原型对象也有原型对象。由此,我们的对象,和对象的原型,以及原型的原型,就构成了一个原型链。

// 核心:子构造函数.prototype = new 父构造函数()

// 创建父构造函数
function Parent(){
    this.name = 'baba';
    this.age = 35;
    this.showName = function(){
        console.log(this.name);
    }
}
// 设置父构造函数的原型
Parent.prototype.friends = ['小名', '小强'];
Parent.prototype.showAge = function(){
    console.log(this.age);
}

// 创建子构造函数
function Child(){

}

// 实现继承
// 同时修改子构造函数的原型的构造器属性 防止执行错误
Child.prototype = new Parent();
Child.prototype.constructor = Child;

// 实例对象
var child = new Child();

console.log(child.name);    // baba
console.log(child.age);     // 35
child.showName();           // baba
child.showAge();            // 35
console.log(child.friends); // ['小名','小强']

// 当我们改变friends时, 父构造函数的原型对象的也会跟着变化
child.friends.push('小王');
console.log(child.friends);     // ["小名", "小强", "小王"]
var father = new Parent();
console.log(father.friends);    // ["小名", "小强", "小王"]

/*--------------------------------------------
	特点:
	- 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
	- 父类新增原型方法/原型属性,子类都能访问到
	问题:
	- 创建子类实例时,不能给父构造函数传递参数
	- 要想为子类新增属性和方法,必须创建实例对象后才行,不能放到构造器中
	- 无法实现多继承
	- 来自原型对象的引用属性是所有实例共享的,子对象修改时父对象也会被动修改
---------------------------------------------*/

6.3.4 借用构造函数

// 使用call借用其他构造函数的成员, 用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类

// 创建父构造函数
function Parent(name, age) {
    this.name = name;
    this.age = age;

}
Parent.prototype.sayName = function () {
    console.log(this.name);
}

// 创建子构造函数
function Child(name, age, sex) {
    Parent.call(this, name, age, sex);
    this.sex = sex;
}

// 创建实例对象
var son = new Child('zs', 18, '男');
console.dir(son); 
// son访问到了Child的sex 还访问到了Parent的name和age 但未访问到Parent原型中的sayName方法

/*-----------------------------------------------
	特点:
    - 解决了传递参数、子类实例共享父类引用属性的问题
    - 可以实现多继承(call多个父类对象)但不完美,没有父类方法
	缺点:
    - 实例并不是父类的实例,只是子类的实例
    - 只能继承父类的实例属性和方法,不能继承原型属性/方法
    - 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
----------------------------------------------*/

6.3.5 组合继承

// 借用构造函数 + 原型式继承

// 创建父构造函数
function Parent(name, age) {
    this.name = name;
    this.age = age;

}
Parent.prototype.sayName = function () {
    console.log(this.name);
}

// 创建子构造函数
function Child(name, age, sex) {
    Parent.call(this, name, age, sex);
    this.sex = sex;
}

// 通过原型,让子类型,继承父类型中的方法
Child.prototype = new Parent();
Child.prototype.constructor = Child;
// 可以赋予子类型特有的方法
Child.prototype.sayHi = function () {
    console.log('你好');
}

/*-------------------------------------------------
	特点:
    - 弥补了借用构造函数的缺陷,可继承实例属性/方法 和 原型属性/方法
    - 既是子类的实例,也是父类的实例
    - 不存在引用属性共享问题
    - 可传参
    - 函数可复用
    - 可以实现多继承
	缺点:
	- 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了,仅仅多消耗了一点内存)
---------------------------------------------------*/

6.3.6 寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路,寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。

6.3.7 寄生组合式继承

寄生组合式继承,集寄生式继承和组合继承的优点与一身,是实现基于类型继承的最有效方式。

6.4 函数进阶

定义函数的方式有两种:函数声明、函数表达式

// 1 函数声明
function fn() {
	console.log('这是函数声明!');
}

// 2 函数表达式
var fn = function () {
	console.log('这是函数表达式');
}

fn();

区别:在执行函数时, fn( ); 的代码顺序对1函数声明没有任何影响,但若把fn( );写于2函数表达式之前,程序会报错。

原因:JS代码是按从上往下顺序链执行的,但在执行前会进行预解析,预先把代码中的函数声明(function)和变量声明(var)提前,内部运行原理如下:

fn1();
function fn1(){};
fn2();
var fn2 = function (){};

===>

function fn1(){};	// 被提前
var fn2;			// 被提前

fn1();				// 运行无问题
fn2();				// 运行 发现首个空变量 运行出错!
fn2 = function (){};	// fn2最后才被赋值 已无效

注意:不要在if else while for等语句中去定义一个函数声明,因为在现代浏览器中,这些语句中函数声明不会被提前,若这样做,运行时容易出错!

6.4.1 函数中this

this 指上下文对象,每次调用函数时,解析器都会将一个this作为隐含参数传递进函数。根据函数调用形式的不同,this的值也不同,函数的调用方式决定了this的指向

调用方式this指向备注
普通函数调用window严格模式下是 undefined
构造函数调用实例对象原型方法中 this 也是实例对象
对象方法调用该方法所属对象紧挨着的对象
事件绑定方法绑定/触发事件对象
定时器函数window

总结:函数内部的this,是由函数被调用时来确定其指向的,即 谁调用指向谁

6.4.2 函数对象

函数其实也是一种特殊的对象,即Function(),它拥有原型,拥有一些属性和方法,且所有函数都是 Function 构造函数的实例

方法

call() bind() apply() 是函数常用的方法,用于改变函数中的this指向

function fn(x, y) {
	console.log(this);	// this原本指向 window
	console.log(x + y);	// 求两数之和
}
fn(5, 6);				// 11

var another = {
    value: "这是另一个对象的值"
}

fn.call(anotherFn, 3, 4);	//返回 "anotherFn对象" 和 "7"
fn.apply(anotherFn, [3, 4]);//返回 "anotherFn对象" 和 "7"
fn.bind(anotherFn, 3, 4);	//运行无反应 无效语句

var other = fn.bind(anotherFn, 3, 4);	//只能先绑定函数
other();	// 再去运行变量	//返回 "anotherFn对象" 和 "7"

/*--------------------------------------------


call():调用一个函数,其具有一个指定的this值和传入提供的参数 apply():调用一个函数,其具有一个指定的this值和作为数组传入提供的参数 bind():创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体,目标函数会重新指定this指向,并传入参数

  • 参数:
    • 第一个参数 设置函数内部this的指向
    • 其它参数,对应函数的参数
  • call函数的返回值:
    • 即指定函数传参后的返回值

  • call()和apply()的区别:call()可接受若干个参数的列表,而apply()接受的是一个包含多个参数的数组

  • call()和bind()的区别:call()改过this的指向后,会再执行函数,bind()改过this后,不会执行调用函数,而是把函数复制一份,返回给一个绑定新this的函数

  • 共同点:一般都用于改变this的指向

// call()的应用

// 对于真正的数组,我们可以用push()和splice()方法对数组元素进行增加或删除,但对于伪数组,这些方法是不可行的,但可以调用call()方法来实现

var fakeArr = {		// 这是一个伪数组
    0: 100,
    1: 10,
    2: 11,
    3: 20,
    length: 4
};

//一般解决方法
fakeArr['4'] = 30;
fakeArr.length ++;

//调用call()方法	(length会自动增减 无需操作)
Array.prototype.push.call(fakeArr, 30);		//增加元素
Array.prototype.splice.call(fakeArr, 0, 3);	//删减元素
// apply()的应用

//Math.max()方法可以求传入数值参数的最大值或者打印出来,但若传入的是数组,它是不可行的,但可以调用apply()方法来实现


console.log(Math.max(3, 5, 6));	// 6 (可以求最大值)

var arr = [5, 10, 1, 3];
console.log(Math.max(arr));// NaN (说明不能求数组中的最大值)

// 调用apply()方法
console.log(Math.max.apply(null, arr)); // 10
console.log(Math.max.apply(Math, arr));	// 10 (指向Math)


console.log(1, 2, 3);	// 1 2 3
console.log(arr);	//(5) [5, 10, 1, 3] (不是我们想要的形式)

// 调用apply()方法 打印
console.log.apply(console, arr);//5 10 1 3 (指向console)
// bind()的应用		常用于改变this的指向

//实现1秒打印对象中的指定属性的值
var obj = {
    value: '1234',
    fn: function() {
    	setInterval(function() {
    		console.log(this.value);
    	}.bind(this), 1000);
    }
}

obj.fn();	// (输出的是空字符串 并未实现打印)

btn.onclick = function () {
// 事件处理函数中的this  是触发该事件的对象
// 通过bind 改变事件处理函数中this的指向
}.bind(obj);	// 1234...1234... (调用bind()实现)

属性

  • arguments 伪数组 获取函数的实参 返回变成数组形式
  • caller 返回函数的调用者,但在全局范围调用时返回 null
  • length 返回形参的个数
  • name 返回函数的名称

6.4.3 高阶函数

  • 函数可以作为参数
  • 函数可以作为返回值
// 作为参数

function eat(fn) {
    setTimeout(function () {
        console.log('吃完了');
        fn();
    }, 1000)
}

eat(function () {console.log('去唱歌');});//吃完了 去唱歌
// 作为返回值 (实际上利用到了"闭包"现象)

// 案例一 返回随机数
function getRandom() {
    var random = parseInt(Math.random() * 10) + 1;
    return function () {
    	return random; 
    }
}
var fn = getRandom();
console.log(fn());	// 7
console.log(fn());	// 7 (两次返回的随机数都不会改变)

// 案例二 求两个数的和
function getFun(n) {
	return function (m) {
		return n + m;
	}
}

var fn100 = getFun(100);	// 求 100 + m
var fn1000 = getFun(1000);	// 求 1000 + m

console.log(fn100(1));		// 101
console.log(fn1000(1));		// 1001

6.5 闭包

闭包:即在一个作用域中可以访问另一个作用域的变量,它延展了函数的作用域范围,可简单理解成 “定义在一个函数内部的函数”,本质上就是将函数内部和函数外部连接起来的一座桥梁

闭包的用途:

  • 解决变量私有化问题

  • 可以在函数外部读取函数内部成员

  • 让函数内成员始终存活在内存中

  • 缓存数据

  • 延长作用域链

全局执行上下文 = { 作用域链:{ 全局变量对象 },{ 变量对象 :a,b } }

// 未发生闭包
function fn() {
    var n = 10;
    return n;
}
fn();

// 发生闭包  (函数嵌套在return中 作为返回值返回)
function fn() {
    var n = 10;
    return function () {
    	return n;
    }
}
var f = fn();
console.log(f());	// 10
/*----------------------------------------
	函数在Global作用域上访问到了Local作用域中的n值,并出现Closure(fn()),这就是闭包,它会作为一个函数来返回
----------------------------------------*/

闭包案例1

<ul id="phone">
    <li>华为</li>
    <li>小米</li>
    <li>vivo</li>
    <li>oppo</li>
</ul>

<script>
// 实现点击li的时候输出当前li对应的索引
var phone = document.getElementById('heroes');
var list = heroes.children;

// 方式1 一般实现		(利用索引)
for (var i = 0; i < list.length; i++) {
	var li = list[i];
	li.index = i;
	li.onclick = function () {
		console.log(this.index);
	}
}

// 方式2 用闭包实现	(利用自调用函数)
for (var i = 0; i < list.length; i++) {
	var li = list[i];
	(function (i) {
		li.onclick = function () {
			console.log(i);
		}
	})(i);
}
</script>

闭包案例2

console.log('start');
setTimeout(function () {
	console.log('timeout');
}, 0);
console.log('over');
// 输出顺序 start over timeout

/*—————————————————————————————————————————————————————
为什么timeout会最后输出?
	原因是setTimeout()本身就是一个函数,它被放到"执行栈"中,运行到它时,它会把内部存在的函数先暂存到"任务列表"中,待"执行栈"中的命令执行完,再去执行"任务列表"中的命令。
————————————————————————————————————————————————————*/
console.log('start');
for (var i = 0; i < 3; i++) {
    setTimeout(function () {
        console.log(i);
    }, 0);
}
console.log('end');
// 输出顺序 start over 3 3 3

console.log('start');
for (var i = 0; i < 3; i++) {
	(function (i) {
		setTimeout(function () {
			console.log(i);
		}, 0);
	})(i);
}
console.log('end');
// 输出顺序 start over 0 1 2 (因为这里调用了自调用函数 产生了闭包 访问到了其他作用域)

闭包案例3

<body>
    *** 改变这行文字的字体大小 ***
    <div id="box">
        <button id="btn1" size="12">按钮1</button>
        <button id="btn2" size="14">按钮2</button>
        <button id="btn3" size="16">按钮3</button>
    </div>
    
<script>
    var btn1 = document.getElementById('btn1');
    var btn2 = document.getElementById('btn2');
    var btn3 = document.getElementById('btn3');

    // 方法1 一般实现
    btn1.onclick = function () {
    	document.body.style.fontSize = '12px';
    }
    btn2.onclick = function () {
    	document.body.style.fontSize = '14px';
    }
    btn3.onclick = function () {
    	document.body.style.fontSize = '16px';
    }


    // 方法2 利用函数闭包实现
    function makeFun(size) {
    	return function () {
    		document.body.style.fontSize = size + 'px';
    	}
    }

    btn1.onclick = makeFun(12);
    btn2.onclick = makeFun(14);
    btn3.onclick = makeFun(16);
    
    // 方法3 利用闭包 避免重复代码
    var box = document.getElementById('box');
    var buttons = box.children;

    for (var i = 0; i < buttons.length; i++) {
    	var btn = buttons[i];	// 为标签添加自定义属性
    	var size = btn.getAttribute('size');
    	btn.onclick = makeFun(size);
    }
</script>

闭包思考

// 案例1
var name = "The Window";
var object = {
	name: "My Object",
	getNameFn: function () {
		return function () {
			return this.name;	//指向全局
		}
	}
}
console.log(object.getNameFn()());	// The Window

// 案例2
var name = "The Window";  
var object = {    
	name: "My Object",
	getNameFn: function () {
		var that = this;		// 改变this指向
		return function () {
			return that.name;	//指向object内部
		}
	}
}
console.log(object.getNameFn()());	// My Object

// getNameFn()() 首个括号仅返回函数 第二个括号返回内部的return值

6.6 递归

递归: 函数自己调用自己

注意:递归一般都要写一个结束的条件,防止过程中出错,造成内存溢出(超过了最大的堆栈大小,类似出现死循环)

递归案例1

// 用递归来实现 1 + 2 + 3 + 4 + .... + n

function getSum(n) {
    
    if (n === 1) {		// 设定递归结束条件
    	return 1;
    }
    return n + getSum(n - 1);
}
console.log(getSum(3));		// 6

/*--------------------------------------------
	***递归内部过程***
	getSum(3)
    	n = 3,	3 + getSum(3 - 1)
    	getSum(3 - 1)
    		n = 2,	2 + getSum(2 - 1)
    		getSum(2 - 1)
    			n = 1,	1
    getSum(3) + getSum(3 - 1) + getSum(2 - 1)	// 3+2+1
    return 6
----------------------------------------------*/

递归案例2

// 用递归实现 n的阶乘 1 * 2 * 3....* n

function fn(n) {
	if (n === 1) {
		return 1;
	}
	return n * fn(n - 1);
}
console.log(fn(3));

// n = 3,  3 * fn(3 - 1)
// n = 2,  2 * fn(2 - 1)
// n = 1,  1

递归案例3

// 用递归实现 斐波那契数列 1、1、2、3、5、8、13、21、34、.....

function fn(n) {
	if (n === 1 || n === 2) {
		return 1;
	}
	return fn(n - 1) + fn(n - 2);
}

console.log(fn(3));		// 2
console.log(fn(5));		// 5

递归案例4

/*----------- 浅拷贝 --------------*/

// 若obj1中拥有 引用类型属性
var obj1 = {
	name: 'zs',
	age: 18,
	sex: '男',
    dog: {		// 为obj1对象里又添加一层对象
    	name: '金毛',
    	age: 2,
    	yellow: '黄色'
    }
}
var obj2 = {};
copy(obj1, obj2);	//调用拷贝函数

// 再去修改obj1的成员
obj1.name = 'xx';	// 修改obj1的成员
obj1.dog.name = '大黄';
console.dir(obj2);	// obj2的name属性没被改变 但是dog.name变了

/*------------------------------------------------------
	原因:因为obj2拷贝过去的dog对象 与 obj1自己的dog对象 都是指向同一个对象,拷贝后,再做修改操作,等于是对深层中的同一个对象进行修改,由于两者共用共享这一对象,所以出现属性改变
	而这里的拷贝,只复制了一层,即只是把第一层(浅层)的属性拷贝过去去生成一个副本,而下一层(深层)的对象,只做指向,并未被拷贝到另一个对象的副本中
	**** 这被称为"浅拷贝" ****
-----------------------------------------------*/
/*----------- 深拷贝 --------------*/

var obj1 = {
    name: 'zs',
    age: 18,
    sex: '男',
    dog: {
    	name: '金毛',
    	age: 2
	},
	friends: ['ls', 'ww']
}
var obj2 = {};

// 封装"深拷贝"函数  把o1的成员拷贝给o2
function deepCopy(o1, o2) {
    for (var key in o1) {
    	var item = o1[key];

        if (item instanceof Object) {	// 若item是对象{}
            o2[key] = {}; 
            deepCopy(item, o2[key]);
        } else if (item instanceof Array) { // 若item是数组[]
            o2[key] = [];
            deepCopy(item, o2[key]);
        } else {		// (否则)若是简单类型
            o2[key] = o1[key];
    	}
    }
}

deepCopy(obj1, obj2);	// 执行深拷贝函数
console.dir(obj2);

// 此时修改obj1中的成员 是否会影响obj2?
obj1.dog.name = 'xxx';
obj1.friends[0] = 'xxx';

console.dir(obj2);		// 发现 obj2的全部属性都没有被改变

/*------------------------------------------------
	这就是"深拷贝",复制对象成员时,把普通属性和对象属性(底层与深层)一起都复制过来,实现完全的拷贝。
--------------------------------------------------*/

(返回 [对象拷贝](#6.3.1 对象拷贝))

递归案例5

// 利用递归 实现遍历DOM树 即遍历某元素下的所有(包括更深层的)子元素
function loadTree(parent) {
	for (var i = 0; i < parent.children.length; i++) {
		var child = parent.children[i];
		// console.log(child); 调试检查 
	loadTree(child);	// 递归 调用自身
	}
}

loadTree(document.body);	// 遍历body标签下的所有子元素
// 实际上对于上例 仅实现遍历的意义并不大 这里赋予它更多的功能
// 传入一个新的功能 去操作遍历完后获取的元素对象 实现定制目的
function loadTree(parent, callback) {
    for (var i = 0; i < parent.children.length; i++) {
    	var child = parent.children[i];
        
    	if (callback) {
			callback(child);
        }
		loadTree(child);	// 递归调用
	}
}

// 遍历完ul列表 并实现点击某项列表项 显示它的内容
var ul = document.getElementById('list');
loadTree(ul, function (element) {
	element.onclick = function () {
		console.log(this.innerText);
	}
});