您的位置:首页 > Web前端 > JavaScript

JavaScript 变量作用域、this、闭包

2016-06-15 17:38 676 查看

JavaScript 变量作用域、this、闭包

变量作用域 scope 和作用域链 scope chain

JavaScript has two scopes: global and local.

A variable that is declared outside a function definition is a global variable, and its value is accessible and modifiable throughout your program.

A variable that is declared inside a function definition is local.

It is created and destroyed every time the function is executed, and it cannot be accessed by any code outside the function.

JavaScript does not support block scope (in which a set of braces {…} defines a new scope), except in the special case of block-scoped variables.

var 变量的作用域和变量提升

JavaScript 有两种作用域:全局和局部。

在函数定义之外声明的变量是全局变量,它的值可在整个程序中访问和修改。

在函数定义内声明的变量是局部变量。每当执行函数时,都会创建和销毁该变量,且无法通过函数之外的任何代码访问该变量。

JavaScript 中,在函数体内 var 声明的变量是函数级作用域,是局部变量,在本函数体内可以访问,而且是在函数体内任意位置可以访问。

// JS 代码
function test() {
console.log(val);
var val = 'this is val';
console.log(val);

func();
function func() {
for (var i = 0; i < 5; i++) {
}
console.log('i: ', i);
console.log('this is func');
}
}

test();


上述代码结果是:

undefined
this is val
i:  5
this is func


JavaScript 解析器预解析代码的时候,
test
函数作如下解析:

function test() {
// 变量提升, 缺省值是 undefined
var val;

// 函数声明提升
function func() {
// 变量提升
var i;

for (i = 0, i < 5, i++) {
}
console.log('i: ', i);
console.log('this is func');
}

console.log(val);
// 变量赋值
val = 'this is val';
console.log(val);

func();
}


所以第一次
console.log(val)
时候并不会抛异常, 因为此时变量
val
是被声明过的,值是
undefined


理解 变量提升 ,写代码时应注意 变量污染 的坑。

如果声明变量时不加 var 直接
val = 1;
, 那么 val 是全局变量。

作用域链

作用域链包含了执行环境有权访问的所有变量和访问顺序。

作为单线程语言的 JavaScript,初始化代码时会创建一个全局上下文,每一次函数调用都会创建一个执行上下文,执行上下文及包含关系:

变量对象

变量

函数声明

参数(arguments)

作用域链

有权访问的变量和访问顺序(本作用域变量和所有父作用域变量)。即函数内部属性
scope
: 本函数有权访问的[变量、对象、函数]的集合

this 值

如下代码:

function func_1() {
var val_1 = 1;
// 抛异常: ReferenceError: val_2 is not defined
console.log(val_1, val_2);

function func_2() {
var val_2 = 2;
// 输出:1 2
console.log(val_1, val_2);
}

func_2();
}

func_1();


简言之, func_1 不能访问 func_2 中声明的变量, func_2 可以访问 func_1 中声明的变量。

当在作用域内访问一个变量
x
时,JavaScript 的查找顺序是这样的:

当前作用域
var x
的定义 => 2.
x
形参 => 3. 函数自身名称是否是
x
=> 4. 上级作用域从 1 开始查找

ES6 中的 let 和 const

ES6 的 let 和 const 实现了块级所用域的变量声明方式,使用 let 和 const 声明变量能有效避免由于变量提升导致的变量污染的问题。

用 let 和 const 声明的变量作用域是代码块,这个设计比较符合大多数人的思维方式。(代码块简单来说就是
{}
大括号包着的区域)

function test() {
if (true) {
var a = 'a';
let b = 'b';
}

// 输出: a
console.log(a);
// 抛异常:ReferenceError: b is not defined
console.log(b);
}

test();


关于 const 的作用有必要正确理解:

严格来说, const 声明了一个指向变量的指针,并不是说 const 声明的变量不可改变, 而是该指针指向的地址不可改变。

MDN 的例子很赞,这里直接拷过来看:

// NOTE: Constants can be declared with uppercase or lowercase, but a common
// convention is to use all-uppercase letters.

// define MY_FAV as a constant and give it the value 7
const MY_FAV = 7;

// this will fail silently in Firefox and Chrome (but does not fail in Safari)
MY_FAV = 20;

// will print 7
console.log("my favorite number is: " + MY_FAV);

// trying to redeclare a constant throws an error
const MY_FAV = 20;

// the name MY_FAV is reserved for constant above, so this will also fail
var MY_FAV = 20;

// MY_FAV is still 7
console.log("my favorite number is " + MY_FAV);

// Assigning to A const variable is a syntax error
const A = 1; A = 2;

// const requires an initializer
const FOO; // SyntaxError: missing = in const declaration

// const also works on objects
const MY_OBJECT = {"key": "value"};

// Overwriting the object fails as above (in Firefox and Chrome but not in Safari)
MY_OBJECT = {"OTHER_KEY": "value"};

// However, object keys are not protected,
// so the following statement is executed without problems
MY_OBJECT.key = "otherValue"; // Use Object.freeze() to make object immutable


this

In most cases, the value of this is determined by how a function is called.

It can’t be set by assignment during execution, and it may be different each time the function is called.

简言之: this 总是指向调用该函数的对象。

全局上下文 global context

// 在浏览器中
console.log(this === window); // true


函数上下文 function context

在函数中访问 this 时, this 指向调用该函数的对象。

1)全局对象

// 全局变量
val = 1;

function test() {
console.log(this.val);
}

test(); // 1


上例中,调用 test 函数的对象并不是一个自己声明的函数或对象,此时 this 默认值为全局对象。

2) 调用对象

var testObj = {
val: 1,
getVal: function() {
var val = 2;
return this.val;
}
};

console.log(testObj.getVal()); // 1


上述代码运行输出
1
, 顺藤摸瓜,
getVal()
函数的调用者是
testObj
对象, 按照
this 指向调用该函数的对象
的原则,
getVal()
中的 this 指向
testObj
对象,
testObj
对象的
val
值是 1.

3) 构造函数

'use strict';

function testFunc(val) {
this.a = val;
this.b = 'bb';
}

var testInstance = new testFunc('aa');

console.log(testInstance.a); // aa
console.log(testInstance.b); // bb


当一个函数的调用者是构造函数(new 出来的对象), this 指向新构造出来的对象
testInstance


4) call and apply

通过 call apply 将 this 指向特定对象:

function testFunc(val) {
this.a = val;
this.b = 'bb';
}

function execFunc() {
var a = 'exec aa';
var b = 'exec bb';

console.log(this.a, this.b);
}

var testInstance = new testFunc('aa');

execFunc.call(testInstance); // aa bb
execFunc.apply(testInstance); // aa bb


通过 call apply 函数将 execFunc 的 this 值指向 testInstance 对象的 this 值。

注意: 以
fun.apply() // or call
为例 call apply 的第一个参数是
func 函数运行时的 this 值
(第一个参数的解释版本真的多)。

二者的区别这里不说。

补充:箭头函数、严格模式下的 this

1) ES6 中的箭头函数 arrow function

An arrow function expression has a shorter syntax compared to function expressions and lexically binds the this value

(does not bind its own this, arguments, super, or new.target).

Arrow functions are always anonymous.

GLOBAL.a = 'global aa';

var testObj = {
a: 'aa',
getValArrowFuc: function() {
var val = (() => this.a);
return val();
},
getVal: function() {
var self = this;
var val = function() {
return self.a;
};
return val();
},
getValGlobal: function() {
var val = function() {
return this.a;
};
return val();
}
};

console.log(testObj.getValArrowFuc()); // aa
console.log(testObj.getVal()); // aa
console.log(testObj.getValGlobal()); // global aa


箭头函数中的 this 值,就是词法作用域的 this 值。

2) 严格模式下的 this

对于一个开启严格模式的函数,指定的 this 不再被封装为对象,而且如果没有指定 this 的话它值是 undefined.

'use strict';
/**
* from MDN
*/
function fun() { return this; }

console.log(fun() === undefined); // true
console.log(fun.call(2) === 2); // true
console.log(fun.apply(null) === null); // true
console.log(fun.call(undefined) === undefined); // true
console.log(fun.bind(true)() === true); // true


注: 以上所有不考虑
Eval


闭包与柯里化

闭包 closure

Closures are functions whose inner functions refer to independent (free) variables.

In other words, the functions defined in the closure ‘remember’ the environment in which they were created.

闭包的构成:

函数

创建该函数的环境,环境由闭包创建时在作用域中的任何局部变量组成

自执行函数表达式写法:

var test = (function() {
var val = 0;

var add = function(num) {
val += num;
return val;
};
return add;
})();

console.log(test(3)); // 3
console.log(test(4)); // 7


个人感觉这个写法可读性更好:

var test = function() {
var val = 0;

var add = function(num) {
val += num;
return val;
};
return add;
};

/**
* 此处 instance 是一个闭包。
* 由 add 函数, 和创建 add 函数时的环境(变量 val)组成
*/
var instance = test();

console.log(instance(3)); // 3
console.log(instance(4)); // 7


以下代码:

function test() {
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 10);
}
}

test();


输出是:

5
5
5
5
5


这里变量 i 的作用域是 test 函数作用域,也就是说
console.log(i)
中的 i 是 test 函数作用域下的同一个变量。

setTimeout 中的函数被执行时,for 遍历已完成并且 i 被赋值为 5.

利用闭包:

function test() {
for (var i = 0; i < 5; i++) {
(function (val) {
setTimeout(() => {
console.log(val);
}, 10);
})(i);
}
}

test();


则会输出:

0
1
2
3
4


这里我们将 i 赋值成一个局部变量,可在闭包内访问(每次循环创建一个闭包, i 作为该闭包作用域下的局部变量,不跟随外层 i 的值改变)。

闭包对性能有负面影响(尤其是内存占用),如果不需要使用,则不使用。

柯里化 curry

什么是柯里化:

简单来说,就是构建一种可以实现参数部分应用的函数。直观点,一个函数需要 n 多参数,通过柯里化实现,我们可以多次调用函数每次只传部分参数,最终实现完整功能。

使用场景:

最典型的场景是要实现一个函数,本函数的一部分参数每次都是一样的。

例子一,hello world:

function hello() {
console.log('hello world');
}

hello(); // hello world


需求改为:say hello to anyone

// curry
function hello() {
return function(name) {
console.log('hello ' + name);
};
}

hello();
hello()('Allen'); // hello Allen
var b = hello();
b('Bob'); // hello Bob
b('John'); // hello John


需求又改为:say anyword to anyone

// curry
function hello(word) {
return function(name) {
console.log(word, ',', name);
};
}

var b = hello('Hi');
b('Bob'); // Hi , Bob
b('John'); // Hi , John

var c = hello('Bye');
c('John'); // Bye , John


需求又改了:word + line + name

// flexible curry !
function hello(word) {
return function(name) {
return function(line) {
console.log(word, ',', name, ',', line);
};
};
}

var b = hello('Hi');
var toBob = b('Bob');
var toAllen = b('toAllen');

toBob('good day tody !'); // Hi , Bob , good day tody !
toBob('Have you eaten yet ?'); // Hi , Bob , Have you eaten yet ?
toAllen('get out of here !'); // Hi , toAllen , get out of here !

var c = hello('Bye');
c('John')('see you tomorrow ~'); // Bye , John , see you tomorrow ~

var d = hello('Hey')('Shawn');
d('this is my daily report .'); // Hey , Shawn , this is my daily report .
d('this is my monthly report .'); // Hey , Shawn , this is my monthly report .


我们最终实现的柯里化函数使用非常灵活。

原文链接
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息