闭包、作用域、函数的4种调用方式
闭包
变量作用域
- 变量作用域的概念:就是一个变量可以使用的范围
- JS中首先有一个最外层的作用域:称之为全局作用域
- JS中还可以通过函数创建出一个独立的作用域,其中函数可以嵌套,所以作用域也可以嵌套
var age=18; //age是在全局作用域中声明的变量:全局变量
function f1(){
console.log(name); //可以访问到name变量
var name="周董" //name是f1函数内部声明的变量,所以name变量的作用域就是在f1函数内部
console.log(name); //可以访问到name变量
console.log(age); //age是全局作用域中声明的,所以age也可以访问
}
//多级作用域
//-->1级作用域
var gender="男";
function fn(){
//gender:可以访问
//age: 可以访问,值为undefined
//height: 不能访问
//-->2级作用域
return function(){
//gender: 通过一级一级作用域的查找,发现gender是全局作用域中声明的变量
//age: 可以访问,值为undefined
//height: 可以访问,值为undefined
console.log(gender);
//-->3级作用域
var height=180;
}
var age=5;
}
- 注意:变量的声明和赋值是在两个不同时期的
function fn(){
console.log(age); //undeinfed
var age=18;
console.log(age); //18
}
- fn函数执行的时候,首先找到函数内部所有的变量、函数声明,把他们放在作用域中,给变量一个初始值:undefined -->变量可以访问
- 逐条执行代码,在执行代码的过程中,如果有赋值语句,对变量进行赋值
作用域链
- 由于作用域是相对于变量而言的,而如果存在多级作用域,这个变量又来自于哪里?我们把这个变量的查找过程称之为变量的作用域链
- 作用域链的意义:查找变量(确定变量来自于哪里,变量是否可以访问)
- 简单来说,作用域链可以用以下几句话来概括:(或者说:确定一个变量来自于哪个作用域)
- 查看当前作用域,如果当前作用域声明了这个变量,就确定结果
- 查找当前作用域的上级作用域,也就是当前函数的上级函数,看看上级函数中有没有声明
- 再查找上级函数的上级函数,直到全局作用域为止
- 如果全局作用域中也没有,我们就认为这个变量未声明(xxx is not defined)
- 再查找上级函数的上级函数,直到全局作用域为止
- 查找当前作用域的上级作用域,也就是当前函数的上级函数,看看上级函数中有没有声明
- 查看当前作用域,如果当前作用域声明了这个变量,就确定结果
function fn(callback){
var age=18;
callback()
}
fn(function(){
console.log(age); //undefined
var age = 15;
//分析:age变量:
//1、查找当前作用域:并没有
//2、查找上一级作用域:全局作用域
//-->难点:看上一级作用域,不是看函数在哪里调用,而是看函数在哪里编写
//-->因为这种特别,我们通常会把作用域说成是:词法作用域
})
闭包概念
各种专业文献上的"闭包"(closure)定义非常抽象,很难看懂。我的理解是,闭包就是能够读取其他函数内部变量的函数。
由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。
所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包的用途
闭包可以用在许多地方。它的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
使用闭包的注意点
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
闭包问题的产生原因
- 函数执行完毕后,作用域中保留了最新的a变量的值
闭包的应用场景
- 模块化
- 防止变量被破坏
函数的4种调用方式
- 在
ES6之前
,函数内部的this是由该函数的调用方式决定的- 函数内部的this跟大小写、书写位置无关
- 在ES6的箭头函数之前的时代,想要判断一个函数内部的this指向谁,就是根据上面的四种方式来决定的
- 1、函数调用
var age=18;
var p={
age:15
say:function(){
console.log(this.age);
}
}
var f1=p.say; //f1是函数
f1(); //函数调用-->this:window -->this.age=18
function Person(name){
this.name=name;
}
Person.prototype={
constructor:Person,
say:function(){
console.log(this.name);
}
}
//函数的第一种调用方式:函数调用
// -->函数内部的this指向window
Person("abc"); //window.name --> abc
//注:window对象中的方法都是全局函数,window对象中的属性都是全局变量
- 2、方法调用
var clear=function(){
console.log(this.length);
}
var length=50;
var tom={ c:clear,length:100 };
tom.c(); //这里是方法调用的方式
//打印this.length 是50 还是100?
//-->相当于:this是指向window还是指向tom呢?
// -->结果为:100
// -->this:tom
//结论:由于clear函数被当成tom.c()这种方法的形式来进行调用,所以函数内部的this指向调用该方法的对象:tom
- 3、new调用(构造函数)
//1、
function fn(name){
this.name=name;
}
//通过new关键字来调用的,那么这种方式就是构造函数的构造函数的调用方式,那么函数内部的this就是该构造函数的实例
var _n=new fn("小明"); //_n有个name属性,值为:小明
//2、
function jQuery(){
var _init=jQuery.prototype.init;
//_init就是一个构造函数
return new _init();
}
jQuery.prototype={
constructor:jQuery,
length:100,
init:function(){
//this可以访问到实例本身的属性,也可以访问到init.prototype中的属性
//这里的init.prototype并不是jQuery.prototype
console.log(this.length); // undefined
}
}
-
4、上下文调用方式(call、apply、bind)
-
上下文模式应用场景:
- 一些需要指定this的情况,比如$.each方法回调函数内部的this
- 判断数据类型:
- Object.prototype.toString.call(1);
-
call、apply
//call、apply function f1(){ console.log(this); } //call方法的第一个参数决定了函数内部的this的值 f1.call([1,3,5]) f1.call({age:20,height:1000}) f1.call(1) f1.call("abc") f1.call(true); f1.call(null) f1.call(undefined); //上述代码可以用apply完全替换 //总结: //call方法的第一个参数: //1、如果是一个对象类型,那么函数内部的this指向该对象 //2、如果是undefined、null,那么函数内部的this指向window //3、如果是数字-->this:对应的Number构造函数的实例 // --> 1 --> new Number(1) //4、如果是字符串-->this:String构造函数的实例 // --> "abc" --> new String("abc") //5、如果是布尔值-->this:Boolean构造函数的实例 // --> false --> new Boolean(false)
- call和apply异同:
- call和apply都可以改变函数内部的this的值
- 不同的地方:传参的形式不同
function toString(a,b,c){ console.log(a+" "+b+" "+c); } toString.call(null,1,3,5) //"1 3 5" toString.apply(null,[1,3,5]) //"1 3 5"
- bind
var obj = { age:18, run : function(){ console.log(this); //this:obj var _that=this; setTimeout(function(){ //this指向window console.log(this.age); //undefined是正确的 console.log(_that.age); //18 },50); } } obj.run(); //bind是es5中才有的(IE9+) var obj5 = { age:18, run : function(){ console.log(this); //this:obj5 setTimeout((function(){ console.log(this.age); //18 }).bind(this),50); //this:obj5 //通过执行了bind方法,匿名函数本身并没有执行,只是改变了该函数内部的this的值,指向obj5 } } obj5.run(); //bind基本用法 function speed(){ console.log(this.seconds); } //执行了bind方法之后,产生了一个新函数,这个新函数里面的逻辑和原来还是一样的,唯一的不同是this指向{seconds:100} var speedBind = speed.bind({ seconds:100 }); speedBind(); //100 (function eat(){ console.log(this.seconds); }).bind({ seconds:360 })() //360 var obj={ name:"西瓜", drink:(function(){ //this指向了:{ name:"橙汁" } console.log(this.name); }).bind({ name:"橙汁" }) } obj.drink(); //"橙汁" var p10={ height:88, run:function(){ //this setInterval((function(){ console.log(this.height); //88 }).bind(this),100) } } p10.run(); ``
-