执行上下文
示例代码:
console.log(a); // undefined
var a = 100;
fn('zhangsan'); // zhangsan 20
function fn(name){
age = 20;
console.log(name,age);
var age;
}
为什么执行结果是这个样子?fn函数调用之前都没有定义,应该调用不到才对呀?
要解释这个现象,我们先来看看执行上下文的规则:
- 范围:
执行上下文是一段script或一个函数 - 全局:
在一段script中,会先把变量定义,函数声明提取出来放到前面 - 函数:
在一个函数中,会先把变量定义,函数声明,this,arguments提取出来放到前面
根据执行上下文规则,示例代码可以解释成这个样子:
var a;
function fn(name){
var age;
age = 20;
console.log(name,age);
}
console.log(a); // undefined
a = 100;
fn('zhangsan'); // zhangsan 20
可以很容易看出,执行结果是没有问题的。
下面我们再来看看"函数声明"和"函数表达式"的不同之处。
函数声明
fn1();
function fn1(){
// 函数声明
console.log('fn1'); // fn1
}
这是函数声明,可以正常显示fn1,不会报错。
因为这段代码相当于:
function fn1(){
// 函数声明
console.log('fn1'); // fn1
}
fn1();
函数表达式
fn2();
var fn2 = function (){
// 函数表达式
console.log('fn2');
}
这是函数表达式,这样是会报错的。
报错信息:Uncaught TypeError: fn2 is not a function
因为这段代码相当于:
var fn2;
fn2();
fn2 = function (){
// 函数表达式
console.log('fn2');
}
因为在执行fn2()的时候,还不知道fn2是个函数,所有会报错。
this
重要:this要在执行时才能确认值,定义时无法确认!!
var name = 'C';
var a = {
name:'A',
fn:function (){
console.log(this.name);
}
}
a.fn(); // this === a,输出结果为:A
a.fn.call({name:'B'}); // this === {name:'B'},输出结果为:B
var fn1 = a.fn;
fn1(); // this === window,输出结果为:C
分析:
- 作为对象属性执行
a.fn()
是对象的一般使用方式,this
指向的是a,所以输出结果是:A - call,apply,bind
a.fn.call({name:'B'})
,this
指向的是call里的参数,也就是{name:'B'}
,所以输出结果是:B - 作为普通函数执行
fn1()
是window对象调用的,this
指向的就是window对象,所以输出对象是:C - 作为构造函数执行
this指向的是构造函数的实例,这里就不再演示了。
补充:call,apply,bind的区别
function fn(name,age){
console.log(name);
console.log(age);
console.log(this);
}
// this指向的是window对象
fn('aaa',10);
// aaa
// 10
// Window
// this指向的是{x:200}
fn.call({x:200},'bbb',20);
// bbb
// 20
// {x: 200}
// this指向的是{x:300},然后参数要以数组形式传递
fn.apply({x:300},['ccc',30]);
// ccc
// 30
// {x: 300}
var fn2 = function(name,age){
console.log(name);
console.log(age);
console.log(this);
}.bind({x:400});
// this指向的是{x:400}
// bind只能用在函数表达式,用在函数声明是不可以的
// 可以向普通函数那样调用
fn2('ddd',40);
// ddd
// 40
// {x: 400}
作用域
!!重要:ES6中的let和const拥有块级作用域,详细参照文章ES6(let和const)
- 没有块级作用域
在其他语言(比如Java),以下情况是会报错的。但是在JS中,由于不存在块级作用域,是不会报错的。
注意,虽然不会报错,但是不要这样写,容易引起误解。
// 无块级作用域
if(true){
var name = 'zhangsan';
}
console.log(name); // zhangsan
- 只有全局和函数作用域
注意:尽量不要在全局作用域中定义变量,容易被污染。
// 函数和全局作用域
var a = 100;
function fn(){
var a = 200;
console.log('fn',a);
}
console.log('global',a); // global 100
fn(); // fn 200
// 以下就是全局变量污染的情况
var a= 300;
console.log('global',a); // global 300
- 作用域链
当前函数作用域内没有定义的变量,会去其父级作用域中去寻找。如果还没有,就会再往上找,直到找到全局作用域,这就叫作用域链。
注意:父级作用域,是指定义函数的作用域,而不是调用函数的作用域。
var a = 100;
function fn(){
var b = 200;
// 当前作用域没有定义的变量,即“自由变量”
console.log(a); // 100
console.log(b); // 200
}
fn();
预解析
var a = 1;
var a = 2;
// 预解析的时候,a是变量,且值为undefined
var a = 1;
function a(){
}
// 预解析的时候,a是函数a()。可以理解为函数的体量比较大,所以就预解析为函数
function a(){
x = 1;
}
funciton a(){
x = 2;
}
// 预解析的时候,a是函数a(),且x = 2
// 这些情况的变量不会预解析
console.log(a); // 报错
a = 1;
let a = 1;
const a = 1;
// 只有var定义的变量,才会预解析
console.log(b); // undefined
var b =1;
// script标签中的预解析是按标签一个一个解析的
// 这种情况下输出1
<script>
var a = 1;
</script>
<script>
console.log(a); // 1
</script>
// 这种情况下会报错
<script>
console.log(a); // 报错
</script>
<script>
var a = 1;
</script>
闭包
闭包的概念:
闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数)
闭包的示例:
// 这就是一个普通的闭包
// 特点:函数b嵌套在函数a内,函数a需要返回函数b
// 用途:
// 1:读取函数内部变量
// 2:让变量i保留在内存中,记住就行,没有为什么
function a(){
var i = 0;
function b(){
console.log(i);
}
return b;
}
var c = a();
c(); // 1
闭包的优缺点:
优点:有利于封装,可以访问局部变量
缺点:内存占用浪费严重,会造成内存泄漏
闭包的使用场景1:函数作为返回值
function F1(){
var a = 100;
// 返回一个函数(函数作为返回值)
return function(){
console.log(a); // a是自由变量,去父级作用域中寻找
}
}
// f1得到一个函数
var f1 = F1();
var a = 200;
f1(); // 100
分析:
console.log(a);
中的a是一个自由变量,所有要在定义函数的父级作用域中寻找。
定义函数的父级作用域时F1函数作用域,所以输出结果是100。
闭包的使用场景2:函数作为参数传递
function F1() {
var a = 100;
// 返回一个函数(函数作为返回值)
return function () {
console.log(a); // a是自由变量,去父级作用域中寻找
}
}
// f1得到一个函数
var f1 = F1();
function F2(fn){
var a = 200;
fn();
}
F2(f1);
常见问题
问题1:请举一个实际开发中闭包应用的例子
// 闭包在实际应用中主要用于封装变量,收敛权限
function isFirstLoad(){
var _list = [];
return function(id){
if(_list.indexOf(id) >= 0){
return false;
}else{
_list.push(id);
return true;
}
}
}
var fn = isFirstLoad();
console.log(fn(100)); // true
console.log(fn(100)); // false
console.log(fn(200)); // true
console.log(fn(200)); // false
注意:
以下滑线开头的变量表示私有变量(比如_list)。
在isFirstLoad函数外面,根本访问不到变量_list,这就是闭包的意义。
提问:
这个list数组之所以在第二次执行firstLoad()的时候仍保存了上一次执行的结果,是因为 isFirstLoad()只执行了一次的原因吗?
回答:
isFirstLoad() 执行了一次,就产生了一个函数作用域,然后 _list 数组就存在这个作用域里面
问题2:创建10个<a>标签 点击的时候弹出对应的序号
- 错误示范:无论点击哪个,弹出的都是10
var i,a;
for(i=0;i<10;i++){
a = document.createElement('a');
a.innerHTML = i + '<br/>';
a.addEventListener('click',function(e){
e.preventDefault();
alert(i); // 自由变量,要去父作用域中获取值
});
document.body.appendChild(a);
}
分析:因为i是自由变量,要去父作用域中获取值。在父作用域中的i是一个全局变量,在For循环运行完之后,值已经变成10了。
- 正确示范1:通过添加自执行函数解决这个问题
var i;
for (i = 0; i < 10; i++) {
(function (i) {
// 函数作用域
var a = document.createElement('a');
a.innerHTML = i + '<br/>';
a.addEventListener('click', function (e) {
e.preventDefault();
alert(i); // 自由变量,要去父作用域中获取值
});
document.body.appendChild(a);
})(i);
}
// 要知道,以上就相当于
(function (i) {
// 函数作用域
var a = document.createElement('a');
a.innerHTML = i + '<br/>';
a.addEventListener('click', function (e) {
e.preventDefault();
alert(i); // 自由变量,要去父作用域中获取值
});
document.body.appendChild(a);
})(1);
(function (i) {
// 函数作用域
var a = document.createElement('a');
a.innerHTML = i + '<br/>';
a.addEventListener('click', function (e) {
e.preventDefault();
alert(i); // 自由变量,要去父作用域中获取值
});
document.body.appendChild(a);
})(2);
// ...依次类推,一直到10...
分析:因为i是自由变量,要去父作用域中获取值。在父作用域中的i是自执行函数的函数作用域里的变量。找到这个变量之后,不会再往上去寻找,所以i的值就对应每个序号了。
- 正确示范2:使用ES6的let定义变量
var a;
for(let i=0;i<10;i++){
a = document.createElement('a');
a.innerHTML = i + '<br/>';
a.addEventListener('click',function(e){
e.preventDefault();
alert(i); // 自由变量,要去父作用域中获取值
});
document.body.appendChild(a);
}
分析:
let定义的变量拥有块级作用域,所以i的值就对应每个序号了。
关于ES6的let和const,参照文章ES6(let和const)
网友评论