主要源于廖雪峰老师的JavaScript教程
1. 函数
- 函数的定义和调用
- 函数声明
#function标明这是一个函数定义
#abs 函数名称
#(x) 函数参数
#{....} 函数体,函数的具体实现
#如果没有return语句,函数返回值为undefined
function abs(x) {
if (x >= 0) {
return x;
} else {
return -x;
}
}
函数也是一个对象,可以用一个变量来指向该函数
# function (x) {...}是一个匿名函数,他没有函数名。但是这个匿名函数赋值给了变量abs。通过abs就可以调用该函数
var abs = function (x) {
...
};
- 函数调用
#按顺序传入参数即可
abs(10); //10
abs(-10); //10
abs(10, 'baababba');//10, 允许传入任意个参数而不影响调用
abs(); //返回NaN,传入参数比定义的少也没有问题,此时函数内部的行参为undefined,计算结果为NaN
#避免收到undefined,可以对参数进行检查:
function abs(x) {
if (typeof x !== 'number') {
throw 'Not a number';
}
if (x >= 0) {
return x;
} else {
return -x;
}
}
- arguments 参数
它只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数。arguments类似Array,但它不是一个Array
'use strict'
function foo(x) {
console.log('x = ' + x ); //10
for (var i = 0; i < arguments.length; i++) {
console.log('arg ' + i + ' = ' + arguments[i]); //10, 20, 30
}
}
foo(10, 20, 30)
#重写abs函数
fuction abs() {
if (arguments.length === 0) {
return 0;
}
var x = arguments[0];
return x >= 0 ? x : -x;
}
#判断入参个数,设置可选参数b
fuction foo (a, b, c) {
if (arguments.length === 2) {
// 实际拿到的参数是a和b,c为undefined
c = b;
b = null;
}
}
- rest 参数
#获取行参个数之外的传入参数
function foo(a, b) {
var i, rest = [];
if (arguments.length > 2) {
for (i = 2; i
- return 语句多行的写法
function foo() {
return { // 这里不会自动加分号,因为{表示语句尚未结束
name: 'foo'
};
}
2. 变量的作用域与解构赋值
- 在JavaScript中,用
var
声明的变量实际上是有作用域的
- 如果一个变量在函数体内部申明,则该变量的作用域为整个函数体,在函数体外不可引用该变量
- 如果两个不同的函数各自申明了同一个变量,那么该变量只在各自的函数体内起作用。换句话说,不同函数内部的同名变量互相独立,互不影响
- 由于JavaScript的函数可以嵌套,此时,内部函数可以访问外部函数定义的变量,反过来则不行
'use strict';
function foo() {
var x = 1;
function bar() {
var y = x + 1; // bar可以访问foo的变量x!
}
var z = y + 1; // ReferenceError! foo不可以访问bar的变量y!
}
- JavaScript的函数在查找变量时从自身函数定义开始,从“内”向“外”查找。如果内部函数定义了与外部函数重名的变量,则内部函数的变量将“屏蔽”外部函数的变量
- 变量提升
JavaScript的函数定义有个特点,它会先扫描整个函数体的语句,把所有申明的变量提升到函数顶部:
'user strict';
function foo() {
var x = 'Hello, ' + y;
console.log(x); //Hello, undefined
var y = 'Bob';
}
foo();
#函数执行过程中并未报错,原因是y变量在稍后申明了。JavaScript自动提升了变量y的申明,但是不会提升变量y的赋值。上述foo()函数,相当于下面的函数:
function foo() {
var y; //提升变量y的申明,此时y为undefined
var x = 'Hello, ' + y;
y = 'Bob';
}
由于这一特性
,因此,在函数内部定义变量时,请严格遵守在函数内部首先申明所有变量
这一规则。
- 全局作用域
不在任何函数内定义的变量就具有全局作用域。实际上, JavaScript默认有一个全局对象window
,全局作用域的变量实际上被绑定到window
的一个属性:
'use strict';
var course = 'Learn JavaScript';
alert(course); //Learn JavaScript
alert(window.course); //Learn JavaScript
#顶层函数也被视为一个全局变量
function foo() {
alert('foo');
}
foo();
window.foo();
#alert() 函数其实也是window的一个变量
window.alert('调用window.alert()');
var old_alert = window.alert; //把alert保存到另一个变量
window.alert = function () {}; //给alert赋一个新函数
alert('无法用alert()显示了!');
window.alert = old_alert; //恢复alert
alert('又可以用alert()了!');
这说明JavaScript实际上只有一个全局作用域。任何变量(函数也视为变量),如果没有在当前函数作用域中找到,就会继续往上查找,最后如果在全局作用域中也没有找到,则报ReferenceError错误。
- 名字空间
全局变量会绑定到window
上,不同的JavaScript文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。
减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。
#唯一的全局变量MYAPP
#其他变量
MYAPP.name = 'myapp';
MYAPP.version = 1.0;
#其他函数:
MYAPP.foo = function () {
return 'foo';
};
把自己的代码全部放入唯一的名字空间MYAPP
中,会大大减少全局变量冲突的可能。
- 局部作用域
由于JavaScript的变量作用域实际上是函数内部,我们无法定义只在for
循环等语句块中可以访问的局部变量:
'use strict';
function foo() {
for (var i = 0; i<100; i++) {}
i += 100; //出了for循环仍然可以访问引用变量i
}
为了解决块级作用域,ES6引入了新的关键字let
,用let
替代var
可以申明一个块级作用域的变量
'use strict';
function foo() {
var sum = 0;
for (let i=0; i<100; i++) {
sum += i;
}
// SyntaxError:
i += 1;
}
- 常量
由于var
和let
申明的是变量,如果要申明一个常量,在ES6之前是不行的,通常用全部大写的变量来表示这是一个常量,不要修改它的值,纯自律
var PI = 3.14;
ES6标准引入了新的关键字const
来定义常量,const
与let
都具有块级作用域:
'use strict';
const PI = 3.14;
PI = 3; //某些浏览器不报错,但是无效果!
PI; //3.14
- 解构赋值
从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'
如果要从一个对象中取出若干属性,也可以使用解构赋值,便于快速获取对象的指定属性
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 person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678',
school: 'No.4 middle school',
address: {
city: 'Beijing',
street: 'No.1 Road',
zipcode: '100001'
}
};
var {name, address: {city, zip}} = person;
name; // '小明'
city; // 'Beijing'
zip; // undefined, 因为属性名是zipcode而不是zip
// 注意: address不是变量,而是为了让city和zip获得嵌套的address对象的属性:
address; // Uncaught ReferenceError: address is not defined
#使用解构赋值对对象属性进行赋值时,如果对应的属性不存在,变量将被赋值为undefined,这和引用一个不存在的属性获得undefined是一致的。如果要使用的变量名和属性名不一致,可以用下面的语法获取:
var person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678',
school: 'No.4 middle school'
};
// 把passport属性赋值给变量id:
let {name, passport:id} = person;
name; // '小明'
id; // 'G-12345678'
// 注意: passport不是变量,而是为了让变量id获得passport属性:
passport; // Uncaught ReferenceError: passport is not defined
#解构赋值还可以使用默认值,这样就避免了不存在的属性返回undefined的问题:
var person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678'
};
// 如果person对象没有single属性,默认赋值为true:
var {name, single=true} = person;
name; // '小明'
single; // true
有些时候,如果变量已经被声明了,再次赋值的时候,正确的写法也会报语法错误:
// 声明变量:
var x, y;
// 解构赋值:
{x, y} = { name: '小明', x: 100, y: 200};
// 语法错误: Uncaught SyntaxError: Unexpected token =
#这是因为JavaScript引擎把{开头的语句当作了块处理,于是=不再合法。解决方法是用小括号括起来:
({x, y} = { name: '小明', x: 100, y: 200});
#使用场景
#1.解构赋值在很多时候可以大大简化代码。例如,交换两个变量x和y的值,可以这么写,不再需要临时变量:
var x=1, y=2;
[x, y] = [y, x]
#2.快速获取当前页面的域名和路径:
var {hostname:domain, pathname:path} = location;
#3.如果一个函数接收一个对象作为参数,那么,可以使用解构直接把对象的属性绑定到变量中。例如,下面的函数可以快速创建一个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 });
// Sun Jan 01 2017 00:00:00 GMT+0800 (CST)
#也可以传入hour、minute和second属性:
buildDate({ year: 2017, month: 1, day: 1, hour: 20, minute: 15 });
// Sun Jan 01 2017 20:15:00 GMT+0800 (CST)
#使用解构赋值可以减少代码量,但是,需要在支持ES6解构赋值特性的现代浏览器中才能正常运行。目前支持解构赋值的浏览器包括Chrome,Firefox,Edge等。
3. 方法
在一个对象中绑定函数,称为这个对象的方法。
var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
var y = new Date().getFullYear();
return y - this.birth;
}
};
xiaoming.age; // function xiaoming.age()
xiaoming.age(); // 今年调用是25,明年调用就变成26了
在一个方法内部,
this
是一个特殊变量,它始终指向当前对象,也就是xiaoming这个变量。所以,this.birth可以拿到xiaoming的birth属性。
如果函数和对象拆开写:
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};
xiaoming.age(); // 25, 正常结果
getAge(); // NaN
单独调用函数getAge()怎么返回了NaN?请注意,我们已经进入到了JavaScript的一个大坑里。
JavaScript的函数内部如果调用了this
,那么这个this到底指向谁?
答案是,视情况而定!
如果以对象的方法形式调用,比如xiaoming.age(),该函数的this指向被调用的对象,也就是xiaoming,这是符合我们预期的。
如果单独调用函数,比如getAge(),此时,该函数的this指向全局对象,也就是window
。
由于这一巨大的设计错误,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(); // Uncaught 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(); // Uncaught TypeError: Cannot read property 'birth' of undefined
结果又报错了!原因是this指针只在age方法的函数内指向xiaoming,在函数内部定义的函数,this又指向undefined了!(在非strict模式下,它重新指向全局对象window!)
修复如下:
'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
}
}
};
xiaoming.age(); // 25
- apply
虽然在一个独立的函数调用中,根据是否是strict
模式,this
指向undefined
或window
, 不过我们还是可以控制this
的指向的!
apply
函数接收两个参数,第一个参数就是需要绑定的this
变量,第二个参数是Array
,表示函数本身的参数。
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};
xiaoming.age(); //25
getAge.apply(xiaoming, []); //25, this 指向xiaoming,参数为空
另一个与apply()
类似的方法是call()
, 唯一的区别是:
apply()
把参数打包成Array
再传入,call()
把参数按顺序传入
Math.max.apply(null, [3, 4, 5]);
Math.max.call(null, 3, 4, 5);
对于普通函数的调用,我们通常把this
绑定为null
.
- 装饰器
利用apply()
,我们还可以动态改变函数的行为。
JavaScript的所有对象都是动态的, 即使内置的函数,我们也可以重新指向新的函数
'use strict';
var count = 0;
var oldParseInt = parseInt; //保存原函数
window.parseInt = function () {
count += 1;
return oldParseInt.apply(null, arguments); //调用原函数
};