奇舞学院及高程学习笔记-函数篇

函数

函数声明与函数表达式

函数声明提升与变量声明提升

函数声明:以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:已失败

    1. 对象的状态不受外界影响。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
    2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
    3. 无法取消Promise
    4. Promise内部抛出的错误,不会反应到外部。
    5. 当处于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传的参数。

猜你喜欢

转载自blog.csdn.net/katecatecake/article/details/79397195