函数
函数声明与函数表达式
函数声明提升与变量声明提升
函数声明:以function开头的函数定义。
js在执行代码之前会先读取函数声明和变量声明,函数声明整个提升,变量赋值在执行时再执行。
console.log([typeof add, typeof sub]); // ['function', 'undefined']
function add(x, y){
return x + y;
}
var sub = function(x, y){
return x - y;
}
console.log([add(1, 2), sub(1, 2)]);
函数声明和var都是没有块级作用域的,所以,以下这种方式请不要使用函数声明,应该使用函数表达式。
if (condition) {
function sayHi() {
alert('Hi!');
}
} else {
function sayHi() {
alert('Yo!')
}
}
除火狐浏览器外,一般都会返回第二个函数,因为再执行之前,函数声明均提升,所以第二个覆盖了第一个。最好使用函数表达式。
函数表达式通常都是匿名函数。
var func = function() {}
console.log(func.name) // func
ES5之后,支持具名的函数表达式。
var func = function num() {}
console.log(func.name) // num
此时,func是存储了一个名为num的函数的变量。
箭头函数
let abc = () => {}
同普通的函数表达式相比,这种形式的this绑定有些区别。
匿名函数(拉姆达函数 )
以为匿名,所以name属性为空字符串。
在函数表达式、回调函数、IIFE中比较常见。
arguments.callee
指向正在执行的函数的指针,比如在递归的时候,使用arguments.callee更安全一些, 有可能函数名会改变。
不过,严格模式下,不能使用arguments.callee,此时,可以使用具名的函数表达式。
函数的参数
function.length(函数参数个数)
可以检验是否传了足够数量的参数,因为有时少了参数,会造成异常。
function __matchArgs__(fn){
return function(...args){
if(args.length !== fn.length){
throw RangeError('Arguments not match!');
}
return fn.apply(this, args);
}
}
var add = __matchArgs__((a, b, c) => a + b + c);
console.log(add(1, 2, 3));
console.log(add(4, 5)); //error,如果没有使用__matchArgs__检查,那么 4+5+undefined会报错。
rest参数
在第零课中提到过,…args是rest参数(ES6),通过这种写法,将参数转化成数组形式传到args变量中,同arguments参数不同,arguments参数是类数组,要用Array.from转成真正的数组。
也可以直接用…args,然后调用数组的归并方法。
注意:…args并不计算在length之中。这么写的话,length为0。
[].slice.call(arguments)
ps:类数组转数组
- Array.from(arguments)
- Array.prototype.slice.call(arguments)
- [].slice.call(arguments)
二三种方式实际上就是写法不同,这两种方式跟slice的底层实现有关,slice的底层实现摘录如下:
if (size > 0) {
cloned = new Array(size);
if (this.charAt) {
for (i = 0; i < size; i++) {
cloned[i] = this.charAt(start + i);
}
} else {
for (i = 0; i < size; i++) {
cloned[i] = this[start + i];
}
}
}
如果不是字符串,即this.charAt为false,那么进入else,以这种方式浅拷贝到数组中,所以如果call(arguments),即可转化为数组。
由判断了this.charAt可以看出,数组和字符串都有这个方法。
参数默认值
function add(x = (function(){throw new Error})(), y = 0) {
return x + y;
}
X默认值是抛出一个错误,则可以让用户至少传入一个参数,y有一个0的默认值,避免只有一个参数时,返回的是NaN。
作用域、闭包、this
闭包
闭包是指有权访问另一个函数作用域中变量的函数,注意与匿名函数区分开。
创建闭包
常见的创建方式,在一个函数内部创建另一个函数。
function createFunc(prop) {
return function(obj1, obj2) {
console.log(prop);
}
}
var newFunc = createFunc(1234);
newFunc(1,2); // 1234
console.log(newFunc.name) // ''
可以看出,即使内部函数被返回,在其他地方被调用了,也还是能访问prop这个变量。这是因为内部函数的作用域链中包含createFunc的作用域。
不过此时就是匿名函数,name也为零,同匿名函数表达式不同。
我们来看看函数被调用的时候都发生了什么。
函数调用时发生的事情
当函数被调用时,会创造一个执行环境及相应的作用域,会自动取得两个特殊变量this和arguments,根据arguments和其他命名参数的值来初始化函数的活动对象,在作用域链中,按内部函数、外部函数、外部的外部等等顺序初始化活动对象和创建作用域,一直到全局执行环境。
一般来说,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域,但是,闭包的情况会稍有不同。
如上面的例子中,函数调用完,将返回的匿名函数赋给了newFunc变量,其执行环境的作用域链会被销毁,但是其活动对象仍会留在内存中,直到匿名函数被销毁。
compareNames = null; // 解除对匿名函数的引用,释放内存
调用时this是global,全局环境中调用的。
闭包的缺点
- 闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存,一般来说绝对必要的时候再使用。
- IE中可能会造成内存泄漏。如果闭包的作用域链中保存着一个HTML元素,那么该元素无法被销毁。
闭包作用域与变量
function createFunctions() {
var result = new Array();
for(var i = 0; i < 10; i++){
result[i] = function(num) {
return (function() {
return num;
})(i);
}
}
return result;
}
通过IIFE,创造了一层作用域,这样setTimeout找i的时候,根据作用域链,就先查找到了这里的i。
如果没有这一层的话,返回时,读到的都是同一个i,因为var是不存在块级作用域的。
此外也可以使用bind,先穿参,不立即执行的一个方法,
function createFunctions() {
var result = new Array();
for(var i = 0; i < 10; i++){
result[i] = function(num) {
return (function() {
return num;
}).bind(null, i);
}
}
return result;
}
闭包与私有数据
var MyClass = (function(){
var privateData = 'privateData';
function Class(){
this.publicData = 'publicData';
}
Class.prototype.getData = function(){
return privateData;
}
return Class;
})();
上面这个是使用IIFE的模式,形成一个函数级作用域。
也可以都改成let、const放在{}里面,形成块级作用域。
this
js是动态语言,this是由函数求值时的调用者决定的。
匿名函数的执行环境具有全局性,this通常指向window。
当然,通过call()、bind()、apply()改变函数执行环境的情况下,this会指向其它对象。
var b = 2;
var obj={
a: function (prop) {
return function(obj1, obj2) {
console.log(this.b);
}
},
b: 123
}
obj.a()() // 2
可以看到,即使是在obj这个环境中调用newFunc所存储的匿名函数,匿名函数内的this仍然是指向全局的。
可以把外部作用域中的this保存在一个闭包能够访问到的变量内,来解决这个问题。
var obj={
a: function (prop) {
let that = this;
return function(obj1, obj2) {
console.log(that.b);
}
},
b: 123
}
obj.a()() // 123
注: this和arguments都存在这个问题,如果想访问作用域中arguments参数,必须将该对象的引用保存到另一个闭包能访问的变量中。
作用域
ES5可以使用IIFE来实现块级作用域
(function() {
// 这里是块级作用域
})()
私有变量
任何函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。
私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。
可以通过闭包创建用于访问私有变量的公有方法。
function MyObject() { // 构造函数
// 私有成员
var privateVariable = 10;
// 特权成员
this.public = function() { // 创建的闭包
console.log(privateVariable);
}
}
let obj = new MyObject();
除实例的public()外,没有任何办法可以直接访问privateVariable。
优点:可以利用这个来隐藏不应该直接被修改的数据。
缺点:因为这种方式必须使用构造函数,所以同构造函数模式相同,每一个实例都要创建一组新方法,可以通过静态私有变量来实现特权方法避免这个问题。
静态私有变量
将public方法放到原型上。
(function(){
...
MyObject.prototype.public = function() {
console.log(...);
}
...
})()
call、bind、apply
func.call(thisObj,arg1,arg2…)、func.apply(thisObj,[obj1,obj2…])
第一个参数改变当前的this,第二个参数为函数传入的参数。
call和apply的差别,仅在于后续参数是数组还是多个写开。
func.bind(thisObj, arg1, arg2)
基本与call相同,差别在于bind返回的是绑定对象和参数后的函数,而非像call和apply那样立即执行,bind之后要赋给一个新的变量,再执行。
执行是还可以接着传参。
function foo(arg1, arg2) {
console.log(arg1, arg2); // 1, 2
}
var bar = foo.bind(null, 1);
bar(2,3); // 1, 2
bind第一个参数为null,表示不改变函数this指向,这么写可以达到先传参但不立即执行的效果。
而且通过bind也可以部分传参,如上面的,可以先传一个参数进去。
如果全部参数传进去,则可以实现延时调用的效果,比如setTimeout就可以先将参数传进去。
异步和回调函数
Promise
promise三种状态
- pending:进行中
- fulfilled:已完成
rejected:已失败
- 对象的状态不受外界影响。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
- 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
- 无法取消
Promise
。 Promise
内部抛出的错误,不会反应到外部。- 当处于
pending
状态时,无法得知目前进展到哪一个阶段。
声明promise对象
// 方法1
let promise = new Promise ( (resolve, reject) => {
if ( /* 异步操作成功 */ ) {
resolve(a) // pending ——> resolved 参数将传递给对应的回调方法
} else {
reject(err) // pending ——> rejectd
}
} )
// 方法2
function promise () {
return new Promise ( function (resolve, reject) {
if ( /* 异步操作成功 */ ) {
resolve(value);
} else {
reject(error);
}
});
}
resolve:将Promise
对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),成功是调用。
reject:将Promise
对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),失败是调用。
方法一是直接实例化,就是立即执行了,方法二是调用promise时再执行。
Promise.prototype.then()和 Promise.prototype.catch()
实例生成后,可以用then
方法分别指定resolved
状态和rejected
状态的回调函数。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
then方法接受两个回调函数作为阐述。第一个是状态变为resolved的时候调用,第二个是变为rejected时调用。第二个参数是可选的。
value和error分别是promise里resolve和reject传的参数。