函数的高级特性(高阶函数)
作用域
背景:在开发中定义变量,你可以在函数内部定义变量,函数内部定义变量只能函数内部使用,函数内部也可以使用外部定义的变量。这种特性是因为作用域造成。
定义:定义了一个变量的可见性和声明周期。
分类:根据变量作用的范围,我们一般将作用域分为两种,全局作用域(代码写在js文件最外层),函数作用域(函数内部 局部)
为什么要有作用域:
- 避免了代码混乱
- 有时候所有对象函数都要用到内容,申明到全局,但是有时候仅仅函数内部使用。用完了就需要销毁。需要全局和局部一起作用。
- 全局变量不容易被回收。
全局变量和局部变量
局部变量就是函数中定义只能函数自己访问。
function add(){
var i = 1;
}
console.info(i);
变量的提升
JavaScript对于变量的定义有一个细节,变量会默认提升。一般在使用变量之前都会先定义这个变量,如果没有定义正常流程就是报错(not defined)
但是在js中如果你先输出变量,在定义变量。默认输出的结果undefined。原因就js中使用var关键字来声明变量,默认会有变量提升。
console.inf9(number);
var number = 10;
当我们浏览器或者node来执行代码的时候,第一步将目前定义好的全局变量先在内存里面定义出来。默认分配的空间为undefined。我们可以先输出在定义。当你执行到 变量定义的代码,这个时候动态将值覆盖内存默认undefined。内存空间会根据你的结果来进行扩容。
变量的提升:var关键字来定义变量。
定义变量的时候,不要将var省略掉。
在js中定义变量的时候没有加上var关键字,默认这个变量全局变量。在不同的运行环境中。这个变量表现不一致。
- 在浏览器运行代码的时候,没有var声明的变量 默认将这个变量绑定window对象。在浏览运行代码的时候,输出this----window窗口对象。
- 在node中运行代码的时候,没有var声明变量,默认将这个变量绑定了全局(global)对象,在node中运行代码的时候。输出this—
变量在函数内部定义,如果要变量提升,提升到函数的顶部。
如果变了是全局作用域,变量提升就是提升到全局作用域顶部。
函数作用域链
背景:定义全局变量的时候,我们函数内部是可以跑到外面来获取这个变量。函数内部还有函数。那他们的变量寻址就遵循作用域链。一层层的外上找。找到就返回。找不到报错。
var number = 10;
function show(){
console.info(number)
}
/**
* 函数内部嵌套函数
*/
var a = 1;
function add(){
var a = 2;
console.info(a);
//内部定义的函数
function cheng(){
console.info(a);
}
cheng();
}
add();
console.info(a);
当我们需要某个变量的时候,js首先会在自己的局部空间里面去寻找变量,找到了就直接使用,如果没有找到变量,则到作用域莲中的第二个作用域去找,一次类推。直到找到这个变量。我们将这种结构称为作用域链。
IIFE
IIFE: Immediately Invoked Function Expression 含义表示立即执行函数。也就是说,在定义函数的时候立即调用。
基础语法
(function(){
})();
(function(){
}())
//IIFE和箭头函数
(()=>{
console.info("小王")
})();
一旦定义IIFE函数,运行代码就立即执行。
IIFE函数名和参数
在IIFE中尽量使用匿名函数,不管传统函数箭头函数。但是你在写代码过程中非的给IIFE里面的执行函数加上函数名字也可以支持。
var a = 2;
(function show(){
console.info(a);
})()
为什么有IIFE
- 早期的js会出现一些问题,js只有函数作用域和全局作用域,并不存在块作用。从ES6开始才有块作用域的概念。ES5中我们可以使用IIFE来模拟出块作用域。减少函数的定义和调用。维护代码的结构
- 有些函数设计出来可能只会执行一次,这种函数你还要单独定义,还要单独调用。直接使用IIFE来进行优化。代码结构友好。
ES6新特
ES6新特性分成两部分:基础部分
ES6进阶部分:
let
在ES6新增了一个关键字let关键字,这个关键字是用来定义变量的。在es5中变量的定义都用var来定义。
作用:新增了一个作用域,弥补了var关键字的不足(历史原因)
使用:直接使用let可以定义变量,如果是在一个块中{},使用let表示这个块的变量。局部变量。
let特点:
- 块级作用域
- 不存在变量提升
- 暂时性死区
- 同一个作用域不能使用let重复定义变量。
块作用域
for(let i=0;i<10;i++){
console.info(i);
}
console.info(i);
for(let i=0;i<10;i++){
// 只控制将函数放在数组中
a[i] = function(){
console.info(i);
}
}
a[0]()
a[1]()
a[2]()
在js循环中,用let关键,变量就变成块作用域,在块中我们每次循环都会将这个变量保存起来。当你要用的时候在找到对应i这样就可以输出i的值。
为什么需要块作用域?
- 解决var定义变量默认提升,在代码中有可能会污染当前这个变量。变量泄露(用完本来就消失,var不会消失)问题。
- 使语法更加规范,设计更加合理。
变量提升
let num = 10;
console.info(num);
使用let的时候不会出现变量提升的情况。在函数里面也不会出现变量提升。
暂时性死区
只要在块级作用域中有let,它所声明的变量就会绑定到这个块区域。不在受外部变量的影响、
什么是暂时性死区(temporal dead zone):
在一个块区间,你先使用变量,在来使用let变量变量。let前面的代码就是一个死区。必须先定义变量在使用。这样就不会产生死区。死区只是用于描述我们编程这种异常的概念。
typeof x
let x;
重复定义
if(true){
let a = 2;
let a = 3;
console.info(a);
}
const
ES6提出 一个关键字,let一样都是用来定义当前程序中需要使用内容。在程序要使用圆周率PI,PI这种值一般是固定不变的。定义一个值,但是这个值在程序一般不会改动。我们将这种在程序中运行过程一直保持边的量称为常量。
常量和变量的区别,变量的值随时改变。let可以用定义变量
const关键字用来定义常量。
const PI = 3.1415926;
let number = 23.5;
number = 35.6; //执行成功。
PI = 4.56789 //报错
在整个程序运行期间,PI值都不能被改变。
作用:定义一个只读的量。这个一般和生活实际相关
特点:一旦const定义过后的值,就不能在更改。具有let大部分的特点
- 变量不提升
- 不允许重复定义
- 块作用域
const a =10;
function show(){
console.info(a);
const a = 20;
}
show();
上面也会产生一个TDZ(暂时性死区)。
规范:
在开发中,一般使用const定义都是常量。为了在开发的时候区分常量和变量,我们会将const定义的量大写。
在代码块中单独定义常量的时候,最好是大写,在循环中没有大写。
const PI = 3.14;
const WEEK = 7;
const a = [1,2,3]
console.info(a.length);
const arr = [1,2,3]
console.info(arr.length);
// arr = null;
console.info(arr);
const arr2 = [1,2,3]
arr2[0] = 45;
console.info(arr2);
const引用一个对象,当前这个常量值不能发生变化,但是引用的对象内部如果发生变化,不影响这个常量。
定义了常量,以后开发要用到这个常量就直接使用,如果初始化改了这个值,一处改处处改。
闭包
闭包在以前的面试中是一种非常重要的点,以前开发很多问题需要使用闭包来解决问题,但是ES6里面有很多方案可以替换闭包。现在闭包也有使用场景,相对来说我们有更好的那就会放弃闭包。
设计一个计数器:
let count = 0;
function add(){
count++;
}
add();
console.inf(count);
function getCount(){
let count = 0;
function add(){
return count++;
}
return add;
}
闭包来实现计数器
/**
* 计数器
*/
function getCount(){
let count = 0;
function add(){
return ++count;
}
return add;
}
//获取到计数器
var count1 = getCount();
console.info(count1())
console.info(count1())
console.info(count1())
var count2 = getCount();
console.info(count2())
console.info(count2())
console.info(count2())
var count3 = getCount();
console.info(count3())
代码剖析:getCount就代表计数器对象,每次调用都相当于将计数器值归零。
调用返回值的时候,其实就在调用函数内部的函数。函数内部的函数就是来执行累加的过程。
主要调用getCount就返回一个新的计数器,原来的计数器按照内容执行自己值。
概念:闭包就是可以访问其他函数中变量的函数。当前函数可以使用其他函数的变量,基于函数的嵌套。内部函数总是可以访问外部函数的参数和变量。即使你外部函数已经执行结束了。
简单来说:函数A里面定义了一个函数B,B函数可以使用A函数的变量的参数,那我们就将这种模式成为闭包。
闭包学习的内容:
- 闭包的模式
- 闭包的作用
- 闭包的优点和缺点
- 闭包的应用
闭包的模式
闭包一共有两种模式:
第一种:函数闭包
第二种:对象闭包
函数闭包:
funtion f1(){
var number = 10;
function f2(){
console.info(number);
}
f2();
}
f1()
对象闭包:
function fun(){
var number = 20;
var obj = {money:number};
console.info(obj.money);
}
fun();
执行fun函数,输出一个20.
函数里面的对象引用了函数里面局部变量,重要对象不销毁,number一直无法销毁。函数空间也无法弹出去。
闭包的内存结构
var username = "xiaowang" //基本类型
var arr = [1,2,3] //引用类型
在内存中,基本数据类型存放在栈空间里面。对象是存放堆空间里面。
栈里面存放了堆里面对象引用(地址)。
var value = 10;
let address = "武侯区"
function f1(){
var number = 10;
let arr = [];
function f2(){
console.info(number);
xxxx();
}
f2();
}
f1()
login();
login(){
if(){
menu();
}
}
栈空间的特点:
- 栈是一种数据结构,内存里面栈采用这种数据结构来保存数据
- 栈满足先入后出,入栈、出栈。栈顶、栈低。
代码执行流程:
- 代码在运行环境中加载的时候,首先会读取代码中的全局变量,var先提升,遇到let按照顺序来定义。在栈里面要创建一个全局的执行环境(在浏览器运行window)。此刻全局作用域里面存放 value、address这些变量。此刻全局作用域在栈底。
- 代码执行f1()的时候,此刻我们栈内存里面就push一个f1的上下文执行空间。在这个空间里面存放一个number变量和一个f2函数的引用。
- 进入f1过后,接下来要执行f2(),此刻在栈内存里面push一个f2的上下文执行环境,f2这个环境可以访问到f1和全局环境里面的变量。获取变量的时候,自顶向下去获取变量。最后到栈底找不到,排除一个异常。
- 随着f2()执行完毕,f2的上下文环境应该被销毁,接着f1的环境也会被销毁。现在就剩下全局环境。最后来销毁。
在开发中,尽量避免全局变量随意定义。
闭包的优点和缺点
优点:闭包可以缓存数据,延长作用链的时间。
缺点:一旦用了闭包,局部变量不能及时的被销毁,会造成一致占用空间的特点。
《JavaScript高级编程》:闭包会携带包含他的作用域,因此会占用比其他程序更多的空间。过度使用闭包内存占用多大。
慎用。
闭包的坑
function outer(){
var result = [];
for(var i=0;i<10;i++){
result[i] = function(){
console.info(i);
}
}
return result;
}
var arr = outer();