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

JavaScript学习记录day6-函数变量作用域、解构赋值与方法

2018-01-26 11:41 921 查看

JavaScript学习记录day6-函数变量作用域、解构赋值与方法

@(学习)[javascript]

JavaScript学习记录day6-函数变量作用域解构赋值与方法
作用域

变量提升

全局作用域

名字空间

局部作用域

常量

解构赋值

方法

apply

装饰器

1. 作用域

在JavaScript中,用
var
申明的变量实际上是有作用域的。

如果一个变量在函数体内部申明,则该变量的作用域为整个函数体,在函数体外不可引用该变量:

'use strict';

function foo() {
var x = 1;
x = x + 1;
}

x = x + 2;   // ReferenceError: x is not defined 无法在函数体外引用变量x


如果两个不同的函数各自申明了同一个变量,那么该变量只在各自的函数体内起作用。换句话说,不同函数内部的同名变量互相独立,互不影响:

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

function bar() {
var x = 'A';
x = x + 'B';
console.log(x);
}

foo();  // 2
bar();  // AB


由于JavaScript的函数可以嵌套,此时,内部函数可以访问外部函数定义的变量,反过来则不行:

function foo() {
var x = 1;
function bar() {
var y = x + 1;  // bar可以访问foo的变量x
}
var z = y + 1;  // ReferenceError: y is not defined, foo不可以访问bar的变量y
}

foo();


如果内部函数和外部函数的变量名重名怎么办?来测试一下:

'user strict';

function foo() {
var x = 1;
function bar() {
var x = 'A';
console.log('x in bar() = ' + x);
}
console.log('x in foo() = ' + x);
bar();
}

foo();
// 结果:
// x in foo() = 1
// x in bar() = A


这说明JavaScript的函数在查找变量时从自身函数定义开始,从“内”向“外”查找。如果内部函数定义了与外部函数重名的变量,则内部函数的变量将“屏蔽”外部函数的变量。

2. 变量提升

JavaScript的函数定义有个特点,它会先扫描整个函数体的语句,把所有申明的变量“提升”到函数顶部:

'use strict';

function foo() {
var x = 'Hello, ' + y;
console.log(x);
var y = 'Bob';
}

foo();


虽然是
strict
模式,但语句
var x = 'Hello, ' + y;
并不报错,原因是变量
y
在稍后申明了。但是
console.log
显示
Hello, undefined
,说明变量
y
的值为
undefined
。这正是因为JavaScript引擎自动提升了变量y的声明,但不会提升变量
y
的赋值。

对于上述
foo()
函数,JavaScript引擎看到的代码相当于:

function foo() {
var y; // 提升变量y的申明,此时y为undefined
var x = 'Hello, ' + y;
console.log(x);
y = 'Bob';
}


由于JavaScript的这一怪异的“特性”,我们在函数内部定义变量时,请严格遵守“在函数内部首先申明所有变量”这一规则。最常见的做法是用一个var申明函数内部用到的所有变量:

function foo() {
var
x = 1, // x初始化为1
y = x + 1, // y初始化为2
z, i; // z和i为undefined
// 其他语句:
for (i=0; i<100; i++) {
...
}
}


3. 全局作用域

不在任何函数内定义的变量就具有全局作用域。实际上,JavaScript默认有一个全局对象
window
,全局作用域的变量实际上被绑定到
window
的一个属性:

'use strict';

var course = 'Learn JavaScript';
console.log(course); // 'Learn JavaScript'
console.log(window.course); // 'Learn JavaScript'


因此,直接访问全局变量
course
和访问
window.course
是完全一样的。

由于函数定义有两种方式,以变量方式
var foo = function () {}
定义的函数实际上也是一个全局变量,因此,顶层函数的定义也被视为一个全局变量,并绑定到
window
对象:

'use strict';

function foo() {
alert('foo');
}

foo(); // 直接调用foo()
window.foo(); // 通过window.foo()调用


我们每次直接调用的alert()函数其实也是window的一个变量:

'use strict';

window.alert('调用window.alert()');
// 把alert保存到另一个变量:
var old_alert = window.alert;
// 给alert赋一个新函数:
window.alert = function () {}

alert('无法用alert()显示了!');

// 恢复alert:
window.alert = old_alert;
alert('又可以用alert()了!');


这说明JavaScript实际上只有一个全局作用域。任何变量(函数也视为变量),如果没有在当前函数作用域中找到,就会继续往上查找,最后如果在全局作用域中也没有找到,则报
ReferenceError
错误。

4. 名字空间

全局变量会绑定到
window
上,不同的JavaScript文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。

减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。例如:

// 唯一的全局变量MYAPP:
var MYAPP = {};

// 其他变量:
MYAPP.name = 'myapp';
MYAPP.version = 1.0;

// 其他函数:
MYAPP.foo = function () {
return 'foo';
};


把自己的代码全部放入唯一的名字空间
MYAPP
中,会大大减少全局变量冲突的可能。

许多著名的JavaScript库都是这么干的:jQuery,YUI,underscore等等。

5. 局部作用域

由于JavaScript的变量作用域实际上是函数内部,我们在
for
循环等语句块中是无法定义具有局部作用域的变量的:

'use strict';

function foo() {
for (var i=0; i<100; i++) {
//
}
i += 100; // 仍然可以引用变量i
}


为了解决块级作用域,ES6引入了新的关键字
let
,用
let
替代
var
可以申明一个块级作用域的变量:

'use strict';

function foo() {
var sum = 0;
for (let i=0; i<100; i++) {
sum += i;
}
// SyntaxError:
i += 1;
}


6. 常量

由于
var
let
申明的是变量,如果要申明一个常量,在ES6之前是不行的,我们通常用全部大写的变量来表示“这是一个常量,不要修改它的值”:

var PI = 3.14;


ES6标准引入了新的关键字
const
来定义常量,
const
let
都具有块级作用域:

'use strict';

const PI = 3.14;
PI = 3; // 某些浏览器不报错,但是无效果!
PI; // 3.14


7. 解构赋值

从ES6开始,JavaScript引入了解构赋值,可以同时对一组变量进行赋值。

什么是解构赋值?我们先看看传统的做法,如何把一个数组的元素分别赋值给几个变量:

var array = ['hello', 'JavaScript', 'ES6'];
var x = array[0];
var y = array[1];
var z = array[2];


现在,在ES6中,可以使用解构赋值,直接对多个变量同时赋值:

'use strict';

// 如果浏览器支持解构赋值就不会报错:
var [x, y, z] = ['hello', 'JavaScript', 'ES6'];

// x, y, z分别被赋值为数组对应元素:
console.log('x = ' + x + ', y = ' + y + ', z = ' + z);


注意:

对数组元素进行解构赋值时,多个变量要用
[...]
括起来。

如果数组本身还有嵌套,也可以通过下面的形式进行解构赋值,注意嵌套层次和位置要保持一致:

let [x, [y, z]] = ['hello', ['JavaScript', 'ES6']];
x; // 'hello'
y; // 'JavaScript'
z; // 'ES6'


解构赋值还可以忽略某些元素:

let [, , z] = ['hello', 'JavaScript', 'ES6']; // 忽略前两个元素,只对z赋值第三个元素
z; // 'ES6'


如果需要从一个对象中取出若干属性,也可以使用解构赋值,便于快速获取对象的指定属性:

'use strict';

var person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678',
school: 'No.4 middle school'
};
var {name, age, passport} = person;

// name, age, passport分别被赋值为对应属性:
console.log('name = ' + name + ', age = ' + age + ', passport = ' + passport);


有些时候,如果变量已经被声明了,再次赋值的时候,正确的写法也会报语法错误:

// 声明变量:
var x, y;
// 解构赋值:
{x, y} = { name: '小明', x: 100, y: 200};
// 语法错误: Uncaught SyntaxError: Unexpected token =


这是因为JavaScript引擎把{开头的语句当作了块处理,于是=不再合法。解决方法是用小括号括起来:

({x, y} = { name: '小明', x: 100, y: 200});


解构赋值使用场景

解构赋值在很多时候可以大大简化代码。例如,交换两个变量
x
y
的值,可以这么写,不再需要临时变量:

var x=1, y=2;
[x, y] = [y, x]


快速获取当前页面的域名和路径:

var {hostname:domain, pathname:path} = location;


如果一个函数接收一个对象作为参数,那么,可以使用解构直接把对象的属性绑定到变量中。例如,下面的函数可以快速创建一个
Date
对象:

function buildDate({year, month, day, hour=0, minute=0, second=0}) {
return new Date(year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second);
}


它的方便之处在于传入的对象只需要
year
month
day
这三个属性:

buildDate({ year: 2017, month: 1, day: 1 });
// 2017-12-31T16:00:00.000Z (UTC)


也可以传入
hour
minute
second
属性:

buildDate({ year: 2018, month: 1, day: 1, hour: 20, minute: 15 });
// 2018-01-01T12:15:00.000Z (UTC)


使用解构赋值可以减少代码量,但是,需要在支持ES6解构赋值特性的现代浏览器中才能正常运行。目前支持解构赋值的浏览器包括Chrome,Firefox,Edge等。

8. 方法

在一个对象中绑定函数,称为这个对象的方法。

在JavaScript中,对象的定义是这样的:

var xiaoming = {
name: '小明',
birth: 1990
};


但是,如果我们给
xiaoming
绑定一个函数,就可以做更多的事情。比如,写个
age()
方法,返回
xiaoming
的年龄:

var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
var y = new Date().getFullYear();
return y - this.birth;
}
};

console.log(xiaoming.age); // [Function: age]
console.log(xiaoming.age()); // 当前调用是28,下一年调用就变成29了


绑定到对象上的函数称为方法,和普通函数也没啥区别,但是它在内部使用了一个
this
关键字。

在一个方法内部,
this
是一个特殊变量,它始终指向当前对象,也就是
xiaoming
这个变量。所以,
this.birth
可以拿到
xiaoming
birth
属性。

让我们拆开写:

function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}

var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};

console.log(xiaoming.age()); // 25, 正常结果
console.log(getAge()); // NaN


JavaScript的函数内部如果调用了
this
,那么这个
this
到底指向谁?

答案是,情况而定!

如果以对象的方法形式调用,比如
xiaoming.age()
,该函数的this指向被调用的对象,也就是
xiaoming
,这是符合我们预期的。

如果单独调用函数,比如
getAge()
,此时,该函数的
this
指向全局对象,也就是
window


如果这么写:

var fn = xiaoming.age; // 先拿到xiaoming的age函数
fn(); // NaN


也是不行的!要保证
this
指向正确,必须用
obj.xxx()
的形式调用!

由于这是一个巨大的设计错误,要想纠正可没那么简单。ECMA决定,在
strict
模式下让函数的
this
指向
undefined
,因此,在
strict
模式下,你会得到一个错误:

'use strict';

var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
var y = new Date().getFullYear();
return y - this.birth;
}
};

var fn = xiaoming.age;
fn(); // TypeError: Cannot read property 'birth' of undefined


这个决定只是让错误及时暴露出来,并没有解决
this
应该指向的正确位置。

有些时候,喜欢重构的你把方法重构了一下:

'use strict';

var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
function getAgeFromBirth() {
var y = new Date().getFullYear();
return y - this.birth;
}
return getAgeFromBirth();
}
};

xiaoming.age(); // TypeError: Cannot read property 'birth' of undefined


结果又报错了!原因是
this
指针只在
age
方法的函数内指向
xiaoming
,在函数内部定义的函数,
this
又指向
undefined
了!(在非
strict
模式下,它重新指向全局对象
window
!)

修复的办法也不是没有,我们用一个
that
变量首先捕获
this


'use strict';

var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
var that = this; // 在方法内部一开始就捕获this
function getAgeFromBirth() {
var y = new Date().getFullYear();
return y - that.birth; // 用that而不是this
}

return getAgeFromBirth();
}
};

console.log(xiaoming.age()); // 28


var that = this;
,你就可以放心地在方法内部定义其他函数,而不是把所有语句都堆到一个方法中。

9.
apply

虽然在一个独立的函数调用中,根据是否是
strict
模式,
this
指向
undefined
window
,不过,我们还是可以控制
this
的指向的!

要指定函数的
this
指向哪个对象,可以用函数本身的
apply
方法,它接收两个参数,第一个参数就是需要绑定的
this
变量,第二个参数是
Array
,表示函数本身的参数。

apply
修复
getAge()
调用:

function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}

var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};

console.log(xiaoming.age()); // 28
console.log(getAge.apply(xiaoming, [])); // 28, this指向xiaoming, 参数为空


另一个与
apply()
类似的方法是
call()
,唯一区别是:

apply()
把参数打包成
Array
再传入;

call()
把参数按顺序传入。

比如调用
Math.max(3, 5, 4)
,分别用
apply()
call()
实现如下:

console.log(Math.max.apply(null, [3, 5, 4])); // 5
console.log(Math.max.call(null, 3, 5, 4)); // 5


对普通函数调用,我们通常把
this
绑定为
null


10. 装饰器

利用
apply()
,我们还可以动态改变函数的行为。

JavaScript的所有对象都是动态的,即使内置的函数,我们也可以重新指向新的函数。

现在假定我们想统计一下代码一共调用了多少次
parseInt()
,可以把所有的调用都找出来,然后手动加上
count += 1
,不过这样做太傻了。最佳方案是用我们自己的函数替换掉默认的
parseInt()


'use strict';

var count = 0;
var oldParseInt = parseInt; // 保存原函数

function parseInt() {
}

window.parseInt = function () {
count += 1;
return oldParseInt.apply(null, arguments); // 调用原函数
};

// 测试:
parseInt('10');
parseInt('20');
parseInt('30');
console.log('count = ' + count); // 3


学习参考教程:http://www.liaoxuefeng.com
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐