ES6和ES7及ES8新特性最新规范知识详细总结

、ECMASript 相关介绍

ECMA概述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hWfWai55-1637595380774)(images/微信截图_20201004101830.png)]

​ Ecma国际(Ecma International)是一家国际性会员制度的信息和电信标准组织。1994年之前,名为欧洲计算机制造商协会(European Computer Manufacturers Association)。因为计算机的国际化,组织的标准牵涉到很多其他国家,因此组织决定改名表明其国际性。

​ 这个组织的目标是评估、开发和认可电信和计算机标准。

​ 该组织在1961年的日内瓦建立为了标准化欧洲的计算机系统。在欧洲制造、销售或开发计算机和电信系统的公司都可以申请成为会员。

ECMAScript概述

​ ECMAScript 是由 Ecma 国际通过 ECMA-262 标准化的脚本程序设计语言。

​ Ecma 国际制定了许多标准,而 ECMA-262 只是其中的一个,所有标准列表查看http://www.ecma-international.org/publications/standards/Standard.htm

​ ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

​ ES6不是新的技术体系,只是JavaScript的最新版本。

ECMAScript 和 JavaScript 的关系

一个常见的问题是,ECMAScript 和 JavaScript 到底是什么关系?

要讲清楚这个问题,需要回顾历史。1996 年 11 月,JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给标准化组织 ECMA,希望这种语言能够成为国际标准。次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript,这个版本就是 1.0 版。

该标准从一开始就是针对 JavaScript 语言制定的,但是之所以不叫 JavaScript,有两个原因。一是商标,Java 是 Sun 公司的商标,根据授权协议,只有 Netscape 公司可以合法地使用 JavaScript 这个名字,且 JavaScript 本身也已经被 Netscape 公司注册为商标。二是想体现这门语言的制定者是 ECMA,不是 Netscape,这样有利于保证这门语言的开放性和中立性。

因此,ECMAScript 和 JavaScript 的关系是,前者是后者的规范,后者是前者的一种实现(另外的 ECMAScript 方言还有 JScript 和 ActionScript)。日常场合,这两个词是可以互换的。

ES6 与 ECMAScript 2015 的关系

ECMAScript 2015(简称 ES2015)这个词,也是经常可以看到的。它与 ES6 是什么关系呢?

2011 年,ECMAScript 5.1 版发布后,就开始制定 6.0 版了。因此,ES6 这个词的原意,就是指 JavaScript 语言的下一个版本。

但是,因为这个版本引入的语法功能太多,而且制定过程当中,还有很多组织和个人不断提交新功能。事情很快就变得清楚了,不可能在一个版本里面包括所有将要引入的功能。常规的做法是先发布 6.0 版,过一段时间再发 6.1 版,然后是 6.2 版、6.3 版等等。

但是,标准的制定者不想这样做。他们想让标准的升级成为常规流程:任何人在任何时候,都可以向标准委员会提交新语法的提案,然后标准委员会每个月开一次会,评估这些提案是否可以接受,需要哪些改进。如果经过多次会议以后,一个提案足够成熟了,就可以正式进入标准了。这就是说,标准的版本升级成为了一个不断滚动的流程,每个月都会有变动。

标准委员会最终决定,标准在每年的 6 月份正式发布一次,作为当年的正式版本。接下来的时间,就在这个版本的基础上做改动,直到下一年的 6 月份,草案就自然变成了新一年的版本。这样一来,就不需要以前的版本号了,只要用年份标记就可以了。

ES6 的第一个版本,就这样在 2015 年 6 月发布了,正式名称就是《ECMAScript 2015 标准》(简称 ES2015)。2016 年 6 月,小幅修订的《ECMAScript 2016 标准》(简称 ES2016)如期发布,这个版本可以看作是 ES6.1 版,因为两者的差异非常小(只新增了数组实例的includes方法和指数运算符),基本上是同一个标准。根据计划,2017 年 6 月发布 ES2017 标准。

因此,ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。本书中提到 ES6 的地方,一般是指 ES2015 标准,但有时也是泛指“下一代 JavaScript 语言”。

语法提案的批准流程

任何人都可以向标准委员会(又称 TC39 委员会)提案,要求修改语言标准。

一种新的语法从提案到变成正式标准,需要经历五个阶段。每个阶段的变动都需要由 TC39 委员会批准。

  • Stage 0 - Strawman(展示阶段)
  • Stage 1 - Proposal(征求意见阶段)
  • Stage 2 - Draft(草案阶段)
  • Stage 3 - Candidate(候选人阶段)
  • Stage 4 - Finished(定案阶段)

一个提案只要能进入 Stage 2,就差不多肯定会包括在以后的正式标准里面。ECMAScript 当前的所有提案,可以在 TC39 的官方网站GitHub.com/tc39/ecma262查看。

ECMAScript 的历史

ECMA-262(ECMAScript)历史版本查看网址

http://www.ecma-international.org/publications/standards/Ecma-262-arch.htm

ES6 从开始制定到最后发布,整整用了 15 年。

前面提到,ECMAScript 1.0 是 1997 年发布的,接下来的两年,连续发布了 ECMAScript 2.0(1998 年 6 月)和 ECMAScript 3.0(1999 年 12 月)。3.0 版是一个巨大的成功,在业界得到广泛支持,成为通行标准,奠定了 JavaScript 语言的基本语法,以后的版本完全继承。直到今天,初学者一开始学习 JavaScript,其实就是在学 3.0 版的语法。

2000 年,ECMAScript 4.0 开始酝酿。这个版本最后没有通过,但是它的大部分内容被 ES6 继承了。因此,ES6 制定的起点其实是 2000 年。

为什么 ES4 没有通过呢?因为这个版本太激进了,对 ES3 做了彻底升级,导致标准委员会的一些成员不愿意接受。ECMA 的第 39 号技术专家委员会(Technical Committee 39,简称 TC39)负责制订 ECMAScript 标准,成员包括 Microsoft、Mozilla、Google 等大公司。

2007 年 10 月,ECMAScript 4.0 版草案发布,本来预计次年 8 月发布正式版本。但是,各方对于是否通过这个标准,发生了严重分歧。以 Yahoo、Microsoft、Google 为首的大公司,反对 JavaScript 的大幅升级,主张小幅改动;以 JavaScript 创造者 Brendan Eich 为首的 Mozilla 公司,则坚持当前的草案。

2008 年 7 月,由于对于下一个版本应该包括哪些功能,各方分歧太大,争论过于激烈,ECMA 开会决定,中止 ECMAScript 4.0 的开发,将其中涉及现有功能改善的一小部分,发布为 ECMAScript 3.1,而将其他激进的设想扩大范围,放入以后的版本,由于会议的气氛,该版本的项目代号起名为 Harmony(和谐)。会后不久,ECMAScript 3.1 就改名为 ECMAScript 5。

2009 年 12 月,ECMAScript 5.0 版正式发布。Harmony 项目则一分为二,一些较为可行的设想定名为 JavaScript.next 继续开发,后来演变成 ECMAScript 6;一些不是很成熟的设想,则被视为 JavaScript.next.next,在更远的将来再考虑推出。TC39 委员会的总体考虑是,ES5 与 ES3 基本保持兼容,较大的语法修正和新功能加入,将由 JavaScript.next 完成。当时,JavaScript.next 指的是 ES6,第六版发布以后,就指 ES7。TC39 的判断是,ES5 会在 2013 年的年中成为 JavaScript 开发的主流标准,并在此后五年中一直保持这个位置。

2011 年 6 月,ECMAScript 5.1 版发布,并且成为 ISO 国际标准(ISO/IEC 16262:2011)。

2013 年 3 月,ECMAScript 6 草案冻结,不再添加新功能。新的功能设想将被放到 ECMAScript 7。

2013 年 12 月,ECMAScript 6 草案发布。然后是 12 个月的讨论期,听取各方反馈。

2015 年 6 月,ECMAScript 6 正式通过,成为国际标准。从 2000 年算起,这时已经过去了 15 年。

目前,各大浏览器对 ES6 的支持可以查看kangax.github.io/compat-table/es6/

Node.js 是 JavaScript 的服务器运行环境(runtime)。它对 ES6 的支持度更高。除了那些默认打开的功能,还有一些语法功能已经实现了,但是默认没有打开。使用下面的命令,可以查看 Node.js 默认没有打开的 ES6 实验性语法。

// Linux & Mac
$ node --v8-options | grep harmony

// Windows
$ node --v8-options | findstr harmony

版本内容

版本 年份 内容变更
第 1 版 1997 年 制定了语言的基本语法
第 2 版 1998 年 较小改动
第 3 版 1999 年 引入正则、异常处理、格式化输出等。IE 开始支持
第 4 版 2007 年 过于激进,未发布
第 5 版 2009 年 引入严格模式、JSON,扩展对象、数组、原型、字符串、日期方法
第 6 版 2015 年 模块化、面向对象语法、Promise、箭头函数、
let、const、数组解构赋值等等
第 7 版 2016 年 幂运算符、数组扩展、 Async/await 关键字
第 8 版 2017 年 Async/await、字符串扩展
第 9 版 2018 年 对象解构赋值、正则扩展
第 10 版 2019 年 扩展对象、数组方法
ES.next 动态指向下一个版本

注:从 ES6 开始,每年发布一个版本,版本号比年份最后一位大 1

ECMA-262标准的维护

TC39(Technical Committee 39)是推进 ECMAScript 发展的委员会。其会员都是公司(其中主要是浏览器厂商,有苹果、谷歌、微软、因特尔等)。TC39 定期召开会议,会议由会员公司的代表与特邀专家出席。

为什么要学习 ES6

​ 目前,前端发展迅猛;JavaScript是前端发展的主要组成部分;ECMAScript标准目前已经发展到了ES11;ES最新版规范增加了很多新特性;

​ **这些新特性语法简洁、功能丰富、而且部分特性可以提升我们的网站性能;**新特性语法已经成为目前前端开发的重要技术,前端三大框架Vue、React、Angular都在使用大量的新特性语法,框架升级也在向新特性语法靠拢;

​ 目前,各大招聘网站对于前端开发人员的要求,其中一个重要指标就是ES6;

​ ⚫ ES6 的版本变动内容最多,具有里程碑意义

​ ⚫ ES6 加入许多新的语法特性,编程实现更简单、高效

​ ⚫ ES6 是前端发展趋势,就业必备技能

ES6 兼容性

http://kangax.github.io/compat-table/es6/ 可查看兼容性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TiakEW5s-1637595380775)(images/微信截图_20201004104136.png)]

二、let和const

JavaScript作用域

什么是作用域

​ 由于代码执行会形成代码执行的空间,这个执行空间指的就是我们的作用域。 表达式,函数执行的环境就会产生作用域,也就是变量和函数能作用到的范围,在这个范围内起作用,它就是所谓的作用域。

function outFun2() {
   
    var inVariable = "内层变量2";
}
outFun2();//要先执行这个函数,否则根本不知道里面是啥
console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined

​ 从上面的例子可以体会到作用域的概念,变量 inVariable 在全局作用域没有声明,所以在全局作用域下取值会报错。我们可以这样理解:作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6 的到来,为我们提供了‘块级作用域’,可通过新增命令 let 和 const 来体现。

全局作用域

函数之外声明的变量,会成为全局变量

全局变量的作用域是全局的:网页的所有脚本和函数都能够访问它。

一般来说以下几种情形拥有全局作用域:

  • 最外层函数和在最外层函数外面定义的变量拥有全局作用域
var outVariable = "我是最外层变量"; //最外层变量
function outFun() {
    //最外层函数
    var inVariable = "内层变量";
    function innerFun() {
    //内层函数
        console.log(inVariable);
    }
    innerFun();
}
console.log(outVariable); //我是最外层变量
outFun(); //内层变量
console.log(inVariable); //inVariable is not defined
innerFun(); //innerFun is not defined
  • 所有末定义直接赋值的变量自动声明为拥有全局作用域
function outFun2() {
   
    variable = "未定义直接赋值的变量";
    var inVariable2 = "内层变量2";
}
outFun2();//要先执行这个函数,否则根本不知道里面是啥
console.log(variable); //未定义直接赋值的变量
console.log(inVariable2); //inVariable2 is not defined
  • 所有 window 对象的属性拥有全局作用域

一般情况下,window 对象的内置属性都拥有全局作用域,例如 window.name、window.location、window.top 等等。

全局作用域有个弊端:如果我们写了很多行 JS 代码,变量定义都没有用函数包括,那么它们就全部都在全局作用域中。这样就会 污染全局命名空间, 容易引起命名冲突。

// 张三写的代码中
var data = {
   a: 100}

// 李四写的代码中
var data = {
   x: true}

这就是为何 jQuery、Zepto 等库的源码,所有的代码都会放在(function(){....})()中。因为放在里面的所有变量,都不会被外泄和暴露,不会污染到外面,不会对其他的库或者 JS 脚本造成影响。这是函数作用域的一个体现。

函数作用域

函数作用域: 由于函数执行,会产生作用域,这块作用域内存放着当前函数内部执行的代码中的变量,函数。

//在这里由于函数person执行,产生一块作用域里面包括name, getName 但是当person函数定义的时候就不会存里面内容,尽管这个函数体是这样的。
function person() {
    
	var name = 'dxb' 
	function getName() {
    
		console.log(name) 
	} 
} 
person();
alert(name); //错误
getName(); //错误

作用域链

通俗地讲,当声明一个函数时,局部作用域一级一级向上包起来,就是作用域链。

1.当执行函数时,总是先从函数内部找寻局部变量

2.如果内部找不到(函数的局部作用域没有),则会向创建函数的作用域(声明函数的作用域)寻找,依次向上

var a = 1;

function fn() {
   
    var a = 10;

    function fn1() {
   
        var a = 20;
        console.log("fn1-->" + a); // 20
    }

    function fn2() {
   
        console.log("fn2-->" + a); // 10
    }
    fn1();
    fn2();
}

fn();
console.log("全局-->" + a); // 1

当执行fn1时,创建函数fn1的执行环境,并将该对象置于链表开头,然后将函数fn的调用对象放在第二位,最后是全局对象,作用域链的链表的结构是fn1->fn->window。从链表的开头寻找变量a,即fn1函数内部找变量a,找到了,结果是20。

同样,执行fn2时,作用域链的链表的结构是fn2->fn->window。从链表的开头寻找变量a,即fn2函数内部找变量a,找不到,于是从fn内部找变量a,找到了,结果是10。

最后在最外层打印出变量a,直接从变量a的作用域即全局作用域内寻找,结果为1。

块级作用域

为什么要有块级作用域

​ 在过去 javascript的变量声明机制大家都已经接触过了,有什么呢,变量声明提升,函数声明整体提升。

​ ECMAScript6新的语法可以更好的控制作用域。块级作用域的出现使得一系列的问题被解决。

回顾var声明提升的机制,先看一段代码

if (false) {
   
    var a = 'wxb'
} else {
   
	console.log("a 的值为" + a)
}
/* 结果是什么呢? a 的值为 undefined 没有报错, 没报错的前提是什么 a被定义 所以在这里的实质就是 */

/*
    var a ;// 在预编译阶段 变量进行的提升
    if(false) { 
        a = 'wxb' 
    } else { 
        console.log("a 的值为" + a) 
    }  
*/
// 接着在看一个例子 
var a = 'dxb';
// ......(此处省略10000行代码) 经过反复的思考觉得姓王也不错呢
var a = 'wxb' // 就把姓改了 姓王了 
console.log(a)
name = 'wxb' // 看到结果,喜闻乐见

值得注意的是:块语句(大括号“{}”中间的语句),如 if 和 switch 条件语句或 for 和 while 循环语句,不像函数,它们不会创建一个新的作用域。在块语句中定义的变量将保留在它们已经存在的作用域中。

1、存在作用域问题。

2、允许重复定义,污染同一作用域下的变量。

为了解决这个问题,引出块级作用域,使得作用域的变量可以更好的把控;

块级作用域

块级作用域可通过新增命令 let 和 const 声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:

  1. 在一个函数内部
  2. 在一个代码块(由一对花括号包裹)内部

let 声明的语法与 var 的语法一致。你基本上可以用 let 来代替 var 进行变量声明,但会将变量的作用域限制在当前代码块中。

let 关键字

let 关键字用来声明变量,使用 let 声明的变量有几个特点:

  • 不允许重复声明
  • 块级作用域
  • 不存在变量提升
  • 不影响作用域链

应用场景:以后声明变量使用 let 就对了

//声明变量
let a;
let b,c,d;
let e = 100;
let f = 521, g = 'iloveyou', h = [];

//1. 变量不能重复声明
// let star = '孙红雷';
// let star = '颜王';

//2. 块儿级作用域  全局, 函数, eval
// if else while for 
// {
   
//     let girl = '王骏迪';
// }
// console.log(girl);

//3. 不存在变量提升
// console.log(song);
// let song = '忠实的心儿想念你';

//4. 不影响作用域链
{
   
    let school = '新开普';
    function fn(){
   
        console.log(school);
    }
    fn();
}
let实例解析
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>点击 DIV 换色</title>
    <link href="https://cdn.bootcss.com/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <style>
        .item {
     
            width: 100px;
            height: 50px;
            border: solid 1px rgb(42, 156, 156);
            float: left;
            margin-right: 10px;
        }
    </style>
</head>

<body>
    <div class="container">
        <h2 class="page-header">点击切换颜色</h2>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item"></div>
    </div>
    <script>
        //获取div元素对象
        let items = document.getElementsByClassName('item');

        //遍历并绑定事件
        for (let i = 0; i < items.length; i++) {
     
            items[i].onclick = function () {
     
                //修改当前元素的背景颜色
                // this.style.background = 'pink';
                items[i].style.background = 'pink';
            }
        }
        // 原理解析,如果是var,当事件回调函数触发的时候,i用的是3
        /* {
            var i = 0;
        }
        {
            var i = 1;
        }
        {
            var i = 2;
        }
        var i = 3;  */

        // 如果是let
        /* {
            var i = 0;
            items[i].onclick = function () {
                //修改当前元素的背景颜色
                // this.style.background = 'pink';
                items[i].style.background = 'pink';
            }
        }
        {
            let i = 1;
            items[i].onclick = function () {
                //修改当前元素的背景颜色
                // this.style.background = 'pink';
                items[i].style.background = 'pink';
            }
        }
        {
            let i = 2;
        } */
    </script>
</body>

</html>

const 声明

const 关键字用来声明常量(不能修改的变量就是常量),const 声明有以下特点

  • 声明必须赋初始值
  • 标识符一般为大写
  • 不允许重复声明
  • 值不允许修改
  • 块级作用域

注意: 对象属性修改和数组元素变化不会出发 const 错误

应用场景:声明对象类型使用 const,非对象类型声明选择 let

//声明常量
const SCHOOL = '新开普';

//1. 一定要赋初始值
// const A;

//2. 一般常量使用大写(潜规则)
// const a = 100;

//3. 常量的值不能修改
// SCHOOL = 'Newcapec'';

//4. 块儿级作用域
// {
   
//     const PLAYER = 'UZI';
// }
// console.log(PLAYER);

//5. 对于数组和对象的元素修改, 不算做对常量的修改, 不会报错
const TEAM = ['UZI', 'MLXG', 'Ming', 'Letme'];
// TEAM.push('XiaoHu');

总结:

  • var:允许重复定义,污染同一作用域下的变量,没有块级作用域
  • let:不允许重复定义,识别块级作用域
  • const:不能重复定义,定义的是常量,识别块级作用域

闭包和递归

函数闭包

闭包概念

**闭包函数:**声明在一个函数中的函数,叫做闭包函数。[不是说所有函数内部的函数都叫闭包函数,闭包函数绝对是函数内部的函数]

**闭包:**内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后。

function outer() {
   
    var a = '变量1'
    var inner = function () {
   
    	console.info(a)
    }
	return inner // inner 就是一个闭包函数,因为他能够访问到outer函数的作用域
}

由于在 Javascript 语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成 “定义在一个函数内部的函数”。

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

闭包的用途:

  • 可以在函数外部读取函数内部成员
  • 让函数内成员始终存活在内存中

很多人会搞不懂匿名函数与闭包的关系,实际上,闭包是站在作用域的角度上来定义的。因为inner访问到outer作用域的变量,所以inner就是一个闭包函数。虽然定义很简单,但是有很多坑点,比如this指向、变量的作用域,稍微不注意可能就造成内存泄露。

我们先把问题抛一边,思考一个问题:为什么闭包函数能够访问其他函数的作用域 ?

从堆栈的角度看待js函数

​ 基本变量的值一般都是存在栈内存中,而对象类型的变量的值存储在堆内存中,栈内存存储对应空间地址。基本的数据类型: Number 、Boolean、Undefined、String、Null。

var a = 1 //a是一个基本类型
var b = {
   m: 20 } //b是一个对象

对应内存存储:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-17OSWoOH-1637595380776)(images/微信截图_20201005101113.png)]

当我们执行 b={m:30}时,堆内存就有新的对象{m:30},栈内存的b指向新的空间地址( 指向{m:30} ),而堆内存中原来的{m:20}就会被程序引擎垃圾回收掉,节约内存空间。我们知道js函数也是对象,它也是在堆与栈内存中存储的,我们来看一下转化:

var a = 1;
function fn(){
   
    var b = 2;
    function fn1(){
   
        console.log(b);
    }
    fn1();
}
fn();

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J9ilCuCj-1637595380779)(images/微信截图_20201005101334.png)]

栈是一种先进后出的数据结构:

1) 在执行fn前,此时我们在全局执行环境(浏览器就是window作用域),全局作用域里有个变量a;

2) 进入fn,此时栈内存就会push一个fn的执行环境,这个环境里有变量b和函数对象fn1,这里可以访问自身执行环境和全局执行环境所定义的变量

3) 进入fn1,此时栈内存就会push 一个fn1的执行环境,这里面没有定义其他变量,但是我们可以访问到fn和全局执行环境里面的变量,因为程序在访问变量时,是向底层栈一个个找,如果找到全局执行环境里都没有对应变量,则程序抛出underfined的错误。

4) 随着fn1()执行完毕,fn1的执行环境被销毁,接着执行完fn(),fn的执行环境也会被销毁,只剩全局的执行环境下,现在没有b变量,和fn1函数对象了,只有a 和 fn(函数声明作用域是window下)

在函数内访问某个变量是根据函数作用域链来判断变量是否存在的,而函数作用域链是程序根据函数所在的执行环境栈来初始化的,所以上面的例子,我们在fn1里面打印变量b,根据fn1的作用域链的找到对应fn执行环境下的变量b。所以当程序在调用某个函数时,做了以下的工作:准备执行环境,初始函数作用域链和arguments参数对象.

我们现在看回最初的例子outer与inner

function outer() {
   
     var  a = '变量1'
     var  inner = function () {
   
            console.info(a)
     }
    return inner    // inner 就是一个闭包函数,因为他能够访问到outer函数的作用域
}
var  inner = outer()   // 获得inner闭包函数
inner() //"变量1"

当程序执行完var inner = outer(),其实outer的执行环境并没有被销毁,因为他里面的变量a仍然被被inner的函数作用域链所引用,当程序执行完inner(), 这时候,inner和outer的执行环境才会被销毁调;《JavaScript高级编程》书中建议:由于闭包会携带包含它的函数的作用域,因为会比其他函数占用更多内容,过度使用闭包,会导致内存占用过多。

闭包特点
  • 让外部访问函数内部变量成为可能;
  • 局部变量会常驻在内存中;
  • 可以避免使用全局变量,防止全局变量污染;
  • 会造成内存泄漏(有一块内存空间被长期占用,而不被释放)
闭包的创建

闭包就是可以创建一个独立的环境,每个闭包里面的环境都是独立的,互不干扰。闭包会发生内存泄漏,**每次外部函数执行的时 候,外部函数的引用地址不同,都会重新创建一个新的地址。**但凡是当前活动对象中有被内部子集引用的数据,那么这个时候,这个数据不删除,保留一根指针给内部活动对象。

闭包内存泄漏为:

​ key = value,key 被删除了 value 常驻内存中;

​ 局部变量闭包升级版(中间引用的变量) => 自由变量;

自由变量:在 A 中作用域要用到的变量 x,并没有在 A 中声明,要到别的作用域中找到他,这个变量 x 就是自由变量。

var x = 20;
function A (b) {
   
    return x + b;
}

A(10);  // 30

上面的都是什么鬼,很多人看到这些话就是一脸懵······

接下来,我们先看例子,看完例子再回看上面的概念,会理解的更!透!彻!

闭包的应用场景

结论:闭包找到的是同一地址中父级函数中对应变量最终的值

最终秘诀就这一句话,每个例子请自行带入这个结论!!!!!!!!!!!!!

例子1

function funA(){
   
    var a = 10;  // funA的活动对象之中;
    return function(){
      //匿名函数的活动对象;
        alert(a);
    }
}
var b = funA();
b();  //10

例子2

function outerFn() {
   
    var i = 0;

    function innerFn() {
   
        i++;
        console.log(i);
    }
    return innerFn;
}
//每次外部函数执行的时候,都会开辟一块内存空间,外部函数的地址不同,都会重新创建一个新的地址
var inner = outerFn();
inner();
inner();
inner();
var inner2 = outerFn();
inner2();
inner2();
inner2();
//1 2 3 1 2 3

例子3

var i = 0;
function outerFn(){
   
    function innnerFn(){
   
        i++;
        console.log(i);
    }
    return innnerFn;
}
var inner1 = outerFn();
var inner2 = outerFn();
inner1();
inner2();
inner1();
inner2();     
//1 2 3 4

例子4

function fn(){
   
    var a = 3;
    return function(){
   
        return  ++a;                                     
    }
}
console.log(fn()());  // 4
console.log(fn()());  // 4

例子5

function outerFn(){
   
    var i = 0;
    function innnerFn(){
   
        i++;
        console.log(i);
    }
    return innnerFn;
}
var inner1 = outerFn();
var inner2 = outerFn();
inner1();
inner2();
inner1();
inner2();    //1 1 2 2

例子6

(function() {
    
    var m = 0; 
    function getM() {
    return m; } 
    function seta(val) {
    m = val; } 
    window.g = getM; 
    window.f = seta; 
})(); 
f(100);
console.info(g());   //100  闭包找到的是同一地址中父级函数中对应变量最终的值

例子7

var lis = document.getElementsByTagName("li");
for(var i=0;i<lis.length;i++){
   
    (function(i){
   
        lis[i].onclick = function(){
   
            console.log(i);
        };
    })(i);       //事件处理函数中闭包的写法
} 

例子8

function m1(){
   
    var x = 1;
    return function(){
   
        console.log(++x);
    }
}

m1()();   //2
m1()();   //2
m1()();   //2

var m2 = m1();
m2();   //2
m2();   //3
m2();   //4

例子9

var  fn=(function(){
   
    var  i=10;
    function  fn(){
   
        console.log(++i);
    }
    return   fn;
})() 
fn();   //11
fn();   //12

例子10

function love1(){
   
    var num = 223;
    var me1 = function() {
   
        console.log(num);
    }
    num++;
    return me1;
}
var loveme1 = love1();
loveme1();   //输出224

例子11:大厂笔试题

function fun(n,o) {
   
    console.log(o);
    return {
   
        fun:function(m) {
   
            return fun(m,n);
        }
    };
}
var a = fun(0);  //undefined
a.fun(1);  //0  
a.fun(2);  //0  
a.fun(3);  //0  
var b = fun(0).fun(1).fun(2).fun(3);   //undefined  0  1  2
var c = fun(0).fun(1);  
c.fun(2);  
c.fun(3);  //undefined  0  1  1

例子12:大厂笔试题

function fn() {
   
    var arr = [];
    for (var i = 0; i < 5; i++) {
   
        arr[i] = function () {
   
            return i;
        }
    }
    return arr;
}
var list = fn();
console.log(list);
for (var i = 0, len = list.length; i < len; i++) {
   
    console.log(list[i]());
} //5 5 5 5 5

例子13:大厂笔试题

function fn() {
   
    var arr = [];
    for (var i = 0; i < 5; i++) {
   
        arr[i] = (function (i) {
   
            return function () {
   
                return i;
            };
        })(i);
    }
    return arr;
}
var list = fn();
for (var i = 0, len = list.length; i < len; i++) {
   
    console.log(list[i]());
} //0 1 2 3 4

函数递归

递归就是调用自身的一种编程技巧,在程序设计中应用广泛。递归函数就是函数对自身的调用,是循环运算的一种算法模式。

JS递归运算

递归必须由以下两部分组成。

  • 递归调用的过程。
  • 递归终止的条件。

在没有限制的情况下,递归运算会无终止地自身调用。因此,在递归运算中要结合 if 语句进行控制,只有在某个条件成立时才允许执行递归,否则不允许调用自身。

递归应用场景

主要解决一些数学运算,如阶乘函数、幂函数和斐波那契数列。

下面示例使用递归运算来设计阶乘函数。

var factorial = function (num) {
   
    if (num <= 1) {
   
        return 1;
    } else {
   
        return num * factorial(num - 1);
    }
}
console.log(factorial(5));  //返回5的阶乘值为120

在这个过程中,利用分支结构把递归结束条件和递归运算分开。

解析递归型数据结构

很多数据结构都具有递归特性,如 DOM 文档树、多级目录结构、多级导航菜单、家族谱系结构等。对于这类数据结构,使用递归算法进行遍历比较合适。

下面使用递归运算计算指定节点内所包含的全部节点数。

function f(n) {
    //统计指定节点及其所有子节点的元素个数
    var l = 0; //初始化计数变量
    if (n.nodeType == 1) l++; //如果是元素节点,则计数
    var child = n.childNodes; //获取子节点集合
    for (var i = 0; i < child.length; i++) {
    //遍历所有子节点
        l += f(child[i]); //递归运算,统计当前节点下所有子节点数
    }
    return l; //返回节点数
}
window.onload = function () {
   
    console.log(f(document.body)); //返回2,即body和script两个节点
}

适合使用递归法解决问题

有些问题最适合采用递归的方法求解,如汉诺塔问题。

下面使用递归运算设计汉诺塔演示函数。参数说明:n 表示金片数;a、b、c 表示柱子,注意排列顺序。返回说明:当指定金片数,以及柱子名称,将输出整个移动的过程。

function f(n, a, b, c) {
   
    if (n == 1) {
    //当为一片时,直接移动
        document.write("移动 【盘子" + n + "】从【" + a + "柱】到【" + c + "柱】<br>");
    } else {
   
        f(n - 1, a, c, b); //调整参数顺序。让参数a移给b
        document.write("移动 【盘子" + n + "】从【" + a + "柱】到【" + c + "柱】<br>");
        f(n - 1, b, a, c); //调整顺序,让参数b移给c
    }
}
f(3, "A", "B", "C"); //调用汉诺塔函数

运行结果如下:

移动【盘子1】从【A柱】到【C柱】
移动【盘子2】从【A柱】到【B柱】
移动【盘子1】从【C柱】到【B柱】
移动【盘子3】从【A柱】到【C柱】
移动【盘子1】从【B柱】到【A柱】
移动【盘子2】从【B柱】到【C柱】
移动【盘子1】从【A柱】到【C柱】
JS尾递归

尾递归是递归的一种优化算法,递归函数执行时会形成一个调用函数,当子一层的函数代码执行完成之后,父一层的函数才会销毁调用记录,这样就形成了调用栈,栈的叠加可能会产生内存溢出。而尾递归函数的每子一层函数不再需要使用父一层的函数执行完毕就会销毁栈记录,避免了内存溢出,节省了内存空间。

示例

下面是阶乘的一种普通线性递归运算。

function f (n) {
   
    return (n == 1) ? 1 : n * f (n - 1);
}
console.log(f(5));  //120

使用尾递归算法后,则可以使用以下方法。

function f (n, a) {
   
    return (n == 1) ? a : f (n - 1, a * n);
}
console.log(f(5, 1));  //120

当 n=5 时,线性递归的递归过程如下。

f (5) = {
   5 * f(4)}
      = {
   5 * {
   4 * f(3) }}
      = {
   5 * {
   4 * {
   3 * f(2)}}}
      = {
   5 * {
   4 * {
   3 * {
   2 * f(1)}}}}
      = {
   5 * {
   4 * {
   3 * {
   2 *1}}}}
      = {
   5 * {
   4 * {
   3 *2}}}
      = {
   5 * {
   4 * 6}
      = {
   5 * 24}
      = 120

而尾递归的递归过程如下。

f (5) = f (5, 1)
    = f (4, 5)
    = f (3, 20)
    = f (2, 60)
    = f (1, 120)
    = 120

很容易看出,普通递归比尾递归更加消耗资源,每次重复的过程调用都使得调用链条不断加长,系统不得不使用栈进行数据保存和恢复,而尾递归就不存在这样的问题,因为它的状态完全由变量 n 和 a 保存。

从理论上分析,尾递归也是递归的一种类型,不过其算法具有迭代算法的特征。上面的阶乘尾递归可以改写为下面的迭代循环。

var n = 5;
var w = 1;
for (var i = 1; i <= 5; i ++) {
   
    w = w * i;
}
console.log(w);

尾递归由于直接返回值,不需要保存临时变量,所以性能不会产生线性增加,同时 JavaScript 引擎会将尾递归形式优化成非递归形式。

JS递归与迭代的区别

递归与迭代都是循环的一种,简单比较如下:

  • 在程序结构上,递归是重复调用函数自身实现循环,迭代是通过循环结构实现。
  • 在结束方式上,递归当满足终止条件时会逐层返回再结束,迭代直接使用计数器结束循环。
  • 在执行效率上,迭代的效率明显高于递归。因为递归需要占用大量系统资源,如果递归深度很大,系统资源可能会不够用。
  • 在编程实现上,递归可以很方便的把数学公式转换为程序,易理解,易编程。迭代虽然效率高,不需要系统开销,但不容易理解,编写复杂问题时比较麻烦。

在实际应用中,能不用递归就不用递归,递归都可以用迭代来代替。

三、解构赋值

解构赋值是对赋值运算符的扩展。

它是一种针对数组或者对象进行模式匹配,然后对其中的变量进行赋值。

在代码书写上简洁且易读,语义更加清晰明了;也方便了复杂对象中数据字段获取。

在解构中,有下面两部分参与:

​ 解构的源,解构赋值表达式的右边部分。

​ 解构的目标,解构赋值表达式的左边部分。

简单来说: 等号 左边的,就是我们要赋值的变量; 等号右边的是我们要赋予的值

var a = 10; a就是要赋值的变量; 10就是我们要赋予的值

数组的解构赋值

解构语法

ES6可以这样为给变量赋值

let [a,b,c] = [1,2,3];//a = 1;b = 2;c = 3;
console.<
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值