js

函数进阶

Posted by liyaozr on March 26, 2017

一、函数定义的三种方式及区别

1、定义方式

1)函数声明
function add(i,j){
    return i+j;
}

特点:

①函数定义会被前置(预解析)

②重复定义函数时,最后一次定义有效

2)函数表达式
var add=function(i,j){
   return i+j;
}
3)函数实例化
var add = new Function("i","j","return (i+j)");

特点:函数实例化定义的函数并不遵循作用域逐级回溯的规则,只能访问本地作用域及全局作用域。

2、函数3种定义方式的区别:

①只有用函数声明定义的函数,可以在函数定义之前被调用,其他方式定义的函数,必须在函数定义之后被调用,否则会报错。

实例1:

//函数一:声明定义
add1(1);           // 函数声明:2
function add1(i){
  console.log("函数声明:"+(i+1));        
}
//函数二:表达式定义
add2(1);           // 报错:add2 is not a function       
var add2 = function(i){
  console.log("函数表达式:"+(i+2));      
} ;
//函数三:实例化定义
add3(1);           //报错: add3 is not a function
var add3 = new Function("i","console.log('函数实例化:'+(i+3));");

在这里,我们把这三种方式定义的函数都进行了提前调用,只有用函数声明方式定义的函数才能正常执行,其他都报错了。

所以,只用函数声明定义的函数,可以在函数声明之前调用。

原因:JS代码的执行顺序:预解析===>执行

②除函数实例化定义的方式以外,其他方式定义的函数都遵循作用域逐级向上回溯的原则。

实例2:

//函数1:
var person = {name:"刘德华", age:50};
(function(){
  // var person = {name:"刘德华", age:30};
  (function() {
    // var person = {name:"刘德华", age:10};
    console.log(person.name+person.age+"岁");        
  })()
})();

//当本地变量存在时,打印出的是刘德华10岁,注释掉本地变量,打印出的是父级变量刘德华30岁,注释掉父级变量,打印出的是全局刘德华50岁。

//函数2:实例化定义
var person = {name:"刘德华", age:50};
(function(){
  var person = {name:"刘德华", age:30};
  var func = new Function("var person={name:'刘德华',age:10};console.log(person.name+person.age+'岁');");
  func();
})();
//当本地变量存在时,打印出的是刘德华10岁,注释掉本地变量,打印出的是全局刘德华50岁。
//原因:通过对象实例化方法定义的所有函数,都会定义在全局作用域上, 因此无法访问到它的父函数上面的所有变量。

③函数被重复定义时的区别

A-函数声明重复定义

add1(1);
function add1(i){
  console.log("函数声明:"+(i+1));
}
function add1(i){
  console.log("函数声明:"+(i+10));
}
add1(1);

输出结果:两次调用都输出:函数声明:11

B-函数表达式重复定义

var add2 = function(i){
  console.log("函数表达式:"+(i+2));
};
add2(1);

var add2 = function(i){
  console.log("函数表达式:"+(i+20));
};
add2(1);
//输出结果:
//函数表达式:3
//函数表达式:21
//两次输出的结果不一致

C-函数实例化重复定义

var add3 = new Function("i","console.log('函数实例化:'+(i+3));");
add3(1);

var add3 = new Function("i","console.log('函数实例化:'+(i+13));");
add3(1);
//输出结果:
//函数表达式:4
//函数表达式:14
//两次输出的结果不一致
//结论:用函数声明的方式定义函数时,最后一次定义有效

扩展:思考三次打印的结果时什么?为什么会这样?

function add1(i){
  console.log("函数声明:"+(i+1));
}
add1(1);

var add1 = function(i){
  console.log("函数表达式:"+(i+10));
};
add1(1);

function add1(i) {
    console.log("函数声明:"+(i+100));
}
add1(1);

输出结果:
函数声明:101
函数表达式:11
函数表达式:11

why???

预解析时,函数声明前置,后面的覆盖前面的,所以此时在JS内存中只有

function add1(i) {
  console.log("函数声明:"+(i+100));
}

这时函数的代码的顺序是这样的:

function add1(i){
  console.log("函数声明:"+(i+100));
}
var add1    (黄色部分被预解析到内存中)

add1(1);
var add1 = function(i){
  console.log("函数表达式:"+(i+10));
};
add1(1);
add1(1);

然后开始逐行读取代码,

add(1);毫无疑问,打印的结果是101;

读到第二行的时候,add1被赋予了新的函数定义,因此后面两行打印结果为11

函数声明和函数表达式定义同一个函数时,执行的是哪个?

假如函数声明在函数表达式之前,那么先执行函数声明,再执行表达式

function add1(i) {
console.log("函数声明:"+(i+100));
}
add1(1); //函数声明:101

var add1 = function(i){
console.log("函数表达式:"+(i+10));
};
add1(1); //函数表达式:11
假如函数声明在函数表达式之后,那么只执行表达式
var add1 = function(i){
console.log("函数表达式:"+(i+10));
};
add1(1); //函数表达式:11

function add1(i) {
console.log("函数声明:"+(i+100));
}
add1(1); //函数表达式:11

二、函数调用模式

1、函数的几种调用方法

谨记:构成函数主体的JS代码在定义之时并不会执行,只有调用该函数时,它们才会执行。

函数调用时,会自动在函数内部调用两个参数:this和arguments参数。根据函数this参数,可以将函数调用分为4类:

* 函数调用模式---this指向window
* 方法调用模式---this指向调用者
* 构造函数调用模式---this指向被构造的对象
* apply(call)调用模式---this指向第一个参数

2、调用方法详解

A. 函数调用模式

函数调用模式当中,在函数内部定义的子函数被调用时,函数内部的this是全局对象,其值仍然指向window,而不是它的上一级对象。

function add(i, j){
  return i+j;
}
var myNumber = {
  value: 1,
  double: function(){
    var helper = function(){
      this.value = add(this.value,this.value);
    };
    helper();
  }
};
B. 方法调用模式

作为对象属性的函数,称为方法。

var myNumber = {
  value: 1,
  add: function(i){
      console.log(this);     //myNumber这个对象
      this.value += i;
  }
}
myNumber.add(1);     //调用

在这里,add函数作为myNumber的属性被调用,我们称之为方法。

这种调用模式为方法调用模式。

C. 构造函数调用模式

new +构造函数来调用,函数内部的this指向被创建的对象

JS提供了9个原生的构造函数,分别是:Boolean、String、Number、Object、Function、Array、Date、RegExp、Error,这些函数首字母都是大写,建议我们在创建构造函数时,也遵循这样的规则。

构造函数与普通函数的区别:

* 本质上没有区别,普通函数也可以被当做构造函数来使用。
* 构造函数通常会有this指定实例属性,原型对象上通常有一些公共方法。
* 构造函数命名通常首字母大写。

构造函数代表一系列对象的属性及行为的封装,代表一系列对象的类型,通常用首字母大写的方式告诉这是一个类型。

function Car(type,color){
  this.type = type;
  this.color = color;
  this.status = "stop";
  this.light = "off";
}
Car.prototype.start = function(){
  this.status = "driving";
  this.light = "on";
  console.log(this.type + " is " + this.status);
}
Car.prototype.stop = function(){
  this.status = "stop";
  this.light = "off";
  console.log(this.type + " is " + this.status);
}
var audi = new Car("audi", "silver");
var benz = new Car("benz", "black");
var ferrari = new Car("ferrari", "yellow");
D. apply(call)调用模式

我们可以将call()和apply()看作是某个对象的方法,通过调用方法的形式来间接调用函数。

call()和apply()的第一个实参是要调用函数的母对象,它是调用上下文,在函数体内通过this来获得它的引用。

apply是Function原型构造函数上的一个方法,所有的函数都可以直接调用apply方法。

apply的作用—函数借用:将函数借用给一个对象,告诉它来实现函数定义的逻辑。所以任何函数都可以作为任何对象的方法来调用,哪怕这个函数不是那个对象的方法。

1)Function.prototype.apply的使用

实例:

//定义一个构造函数Point
function Point(x, y){
    this.x = x;
    this.y = y;
}
// 给Point定义了move这个方法
Point.prototype.move = function(x, y) {
    this.x += x;
    this.y += y;
}  
//move实现的功能很简单,将我们所定义的点做一个位置上的移动   
var p = new Point(0,0);  // 定义了一个点(0,0)
p.move(2,3);             // 用move方法将P点移动到(2,3)的位置

//接下来,我们创造了一个圆
var circle = {x:1, y:1, r:1};
//现在我们也想将圆进行移动,但是在这个对象的原型链上并没有任何能够使它属性进行更改的方法,这时我们就可以使用apply
 p.move.apply(circle, [2, 1]);   //将move方法借用给circle来实现位置的移动  // {x:3,y:2,r:1}  

3、函数调用模式的区别

函数在执行的时候,JS引擎会自动在函数的本地作用域添加this、arguments这两个临时变量,调用模式的本质区别,就在于this的指向。

实例1:函数调用模式


function add(i,j){
  console.log(this);        // window
  // console.log(arguments);
  var sum = i+j;
  console.log(sum);
  //在add函数内部用匿名函数的方式定义了一个嵌套函数
  (function(){
      console.log(this);    // window
  })();
  return sum;
}
add(1,2);
//函数调用模式下,函数本地作用域的this指向的是window全局对象

实例2:方法调用模式

var myNumber = {
  value: 1,
  add: function(i){
      console.log(this);    //  object--myNumber
      this.value += i;
  }
}
myNumber.add(1);
//方法调用模式下,this指向的是方法的调用者

实例3:构造函数调用模式

function Car(type,color){
  this.type = type;
  this.color = color;
  this.status = "stop";
  this.light = "off";
  console.log(this);    // Car {type: "benz", color: "black", status: "stop", light: "off"}
}
Car.prototype.start = function(){
  this.status = "driving";
  this.light = "on";
  console.log(this.type + " is " + this.status);
}
Car.prototype.stop = function(){
  this.status = "stop";
  this.light = "off";
  console.log(this.type + " is " + this.status);
}
var benz = new Car("benz", "black");
//构造函数调用模式下,this指向的是被构造的对象

实例4:apply调用模式

this指向的是第一个参数,代码在apply(call)调用模式中

代码实现原理:将move函数中的this的指向变更为circle

思考题

1.helper函数中this指向哪个对象?

2.这段代码能否实现正确逻辑?

3.在不放弃helper函数的前提下,有哪些修改方法可以实现正确的逻辑?

var myNumber = {
  value: 1,
  add: function(i){
    var helper = function(i){
          console.log(this);
          this.value += i;
    }
    helper(i);
  }
}
myNumber.add(1);

1.this指向window,helper这个函数相当于是定义在全局上的

2.不能,因为window没有value这个属性

3.利用that及其他办法

//方法一:可以把helper调整为方法函数,这样helper就可以正确引用myNumber为this了
    var myNumber = {
        value:1,
        helper:function(i) {
               console.log(this);
               this.value +=i;
        },
        add: function(i) {
             this.helper(i);
        }
       };
    myNumber.add(1);

 //方法二:使用闭包
    var myNumber = {
        value: 1,
        add: function(i){
           var thisnew = this;
           // 构建闭包
           var helper = function(i){
               console.log(thisnew);
               thisnew.value += i;
           };
           helper(i);
         }
    };


 //方法三:使用apply(call)调用模式,将当前helper方法借用给myNumber对象使用,这样this指向的就是myNumber对象
    var myNumber = {
        value: 1,
        add: function(i){
           var helper = function(i){
               console.log(this);
               this.value += i;
           };
           // myNumber对象借用helper方法,helper中的this将指向myNumber对象
           helper.apply(myNumber,[i]); //apply方法
           helper.call(myNumber,i);  //call方法
        }
    };