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

JavaScript中的引用类型Function类型学习心得

2016-12-13 10:48 399 查看

1没有重载

将函数名想象为指针,有助于理解为什么ECMAScript中没有函数重载的概念

function A(num){

    return num + 100;

}
function A(num){

    returnnum + 200;

}
console.log(A(100))//300
显然后面的函数覆盖了前面的函数
以上代码和下面的代码没什么区别
var A=function(num){

    return num + 100;

}

 A=function(num){

    return num + 200;

}
console.log(A(100))
 
第二个函数覆盖了第一个函数的变量A

2函数声明和函数表达式,Function构造函数

解析器在想执行环境家在数据时,对函数声明和函数表达式并非一视同仁,他会率先读取函数声明,并使其在执行任何代码之前可以访问(变量提升),至于函数表达式,则必须等到解析器执行到它所在的代码行才会真正被执行。

console.log(A(10,10));
function A(a,b){

    return a+b;

}
不会报错,解析器已经通过一个名为函数声明提升的过程,读取并将函数声明添加到执行环境,并放到代码树的顶部,即使在最后面也会提升到顶部。然而函数表达式却不同,会报错。
console.log(A(10,10));
var A=function(a,b){

    return a+b;

}
错误信息:
TypeError: A is not a function
var print= function x(){

    console.log(typeof x);

};

x
// ReferenceError: x is not defined

print()
// function
上面代码在函数表达式中,加入了函数名x。这个x只在函数体内部可用,指代函数表达式本身,其他地方都不可用。这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)。因此,下面的形式声明函数也非常常见。
Function构造函数方式创造
var add =new Function(

    'x',

    'y',

    'return (x+y)'
);

等同于
function add(x,y){

    return (x+y);

}
在上面代码中,Function构造函数接受三个参数,除了最后一个参数是add函数的“函数体”,其他参数都是add函数的参数。
你可以传递任意数量的参数给Function构造函数,只有最后一个参数会被当做函数体,如果只有一个参数,该参数就是函数体。
var foo =new Function(

    'return "hello world"'
);
// 等同于
function foo() {

    return'hello world';

}
函数可以调用自身,这就是递归(recursion)。下面就是通过递归,计算斐波那契数列的代码。
function fib(num) {

    if (num > 2) {

        return fib(num - 2)+ fib(num - 1);

    } else {

        return 1;

    }

}
fib(6) // 8
上面代码中,fib函数内部又调用了fib,计算得到斐波那契数列的第6个元素是8。
 
 

3作为值的函数

函数本身就是变量,所以函数也可以作为值来使用,也就是说,不仅可以想传参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。

function A(testfunction,C){
    return testfunction(C);
}
function add(num){
    return num+10;
}
console.log(A(add,10));

也可以从一个函数返回另一个函数,而且这也是极为有用的一种技术,

function creat(pro){
    return function (obj1,onj2) {
        var v1 = obj1[pro];
        var v2 = obj2[pro];

        if(v1<v2){
            return -1;
        }else if(v1>v2){
            return 1;
        }else{
            return 0;
        }
    }
}

在内部函数接收到pro参数后,他会使用方括号标示法来取得给定属性的值。取得了想要的属性值后,定义比较函数就简单了。

var data=[{"name":"zds",age:30},{"name":"ads",age:40}];
data.sort(creat("name"));
console.log(data[0].name);//ads

data.sort(creat("age"));
console.log(data[0].name);//zds

4函数内部属性

在函数内部,有两个特殊的对象,arguments和this。arguments是一个类数组对象,包含着传入函数中的所有参数。这个arguments 还有一个名叫callee的属性,该属性是一个指针,指向拥有这个arguments对象的函数

function factorial(num){
    if(num<=1){
        return 1;
    }else{
        return num*factorial(num-1);
    }
}

这个求阶乘的函数这样定义是有问题的,函数的执行和函数名紧紧的耦合在一起了,如下改动

function factorial(num){
    if(num<=1){
        return 1;
    }else{
        return num*factorial(num-1);
    }
}
var s = factorial;
factorial= function () {
    return 0;
};
console.log(s(5));    //0

如果改为arguments.callee

function factorial(num){
    if(num<=1){
        return 1;
    }else{
        return num*arguments.callee(num-1);
    }
}
var s = factorial;
factorial= function () {
    return 0;
};
console.log(s(5));   //120

在此,s获得了factorial的值实际上是另一个位置尚保存了一个函数的指针。然后,我们又将一个简单的返回0的函数复制给factorial变量,解除耦合之后,仍能正常的计算阶乘。

函数内部另一个特殊的对象是this,其行为于java的this相似,this

引用的是函数数据以执行的环境对象(在网页this就是window)

window.color='red';
var o = {color:'blue'};

function say(){
    console.log(this.color);
}
say();//red

o.say = say;
console.log(o.say());//blue

 

es5引如另一个函数对象属性:caller。如果是全局作用于调用了当前函数,他的值为null,如下:

function outer(){
    inner();
}
function inner(){
    console.log(inner.caller);
}
console.log(outer());//[Function: outer]
                     //undefined

也可以实现更松散的耦合

function outer(){
    inner();
}
function inner(){
    console.log(arguments.callee.caller);
}
console.log(outer());//[Function: outer]
                     //undefined

 

5函数属性和方法

函数是对象,函数也有属性和方法。每个函数都包含两个属性:length和prototype,length表示当前函数接受参数的个数,prototype属性是保存所有实例方法的真正所在。换句话说,诸如tostring(),valueof()等方法实际上都保存在prototype下,只不过是通过各自独享的实例访问,在创建自定义引用类型以及实现继承时,prototype属性的作用是极为重要的,prototype属性是不可枚举的,因此使用for-in无法发现

每个函数都包含两个非继承的方法apply和call。改变函数内this的指向。Apply()方法接受两个参数:一个是在其中运行函数的作用域,另一个是参数数组。其中第二个参数可以是array的实例,也可以是arguments对象。

function sum(n1,n2){
    return n1+n2;
}
function callsum(n1,n2){
    return sum.apply(this,arguments);
}
function callsum2(n1,n2){
    return sum.apply(this,[n1,n2]);
}
console.log(callsum(10,10));
console.log(callsum2(10,10));

call()方法和apply()方法相同,他们的区别是接受参数的方式不同,对于call()方法。第一个参数是this不变,变化的是其余参数直接传递给函数。换句话说,在使用call时,传递给函数的参数必须做个列举出来,如下

function callsum(n1,n2){
    return sum.call(this,n1,n2);
}

至于是使用apply还是call,完全决定你采用哪种方式穿参数,如果你打算直接传入arguments对象,那么使用apply,否则使用call

事实上,apply真正的用武质地是能够扩充函数赖以运行的作用域.如下

window.color = 'red';
var o = {color:'blue'};
function say(){
    console.log(this.color);
}
say();
say().call('1===',this);//red
say().call('2===',window);//red
say().call('3===',o);//blue

使用call和apply来扩充作用于的最大好处就是对象不需要与方法有任何耦合关系。

Array.prototype.slice.call()//可以将arguments这种伪数组转化为真正的数组

 

Es5还有一个bind()方法,这个方法会创建一个函数的实例,其this值会被绑定到传给bind()函数的值,如下:

window.color = 'red';
var o = {color:'blue'};
function say(){
    console.log(this.color);
}
var onj1 = say.bind(o);
onj1();//blue

bind传入o参数,返回一个函数,这个返回的函数的this值等于o

varobj = {
    x: 81,
};  
varfoo = {
    getX: function() {
        returnthis.x;
    }
}  
console.log(foo.getX.bind(obj)());  //81
console.log(foo.getX.call(obj));    //81
console.log(foo.getX.apply(obj));  //81

三个输出的都是81,但是注意看使用 bind() 方法的,他后面多了对括号。
  也就是说,区别是,当你希望改变上下文环境之后并非立即执行,而是回调执行的时候,使用 bind() 方法。而 apply/call 则会立即执行函数。

再总结一下:
       apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;
       apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;
       apply 、 call 、bind 三者都可以利用后续参数传参;
              bind 是返回对应函数,便于稍后调用;apply、call 则是立即调用 。

 

6不能在条件语句中声明函数

根据ECMAScript的规范,不得在非函数的代码块中声明函数,最常见的情况就是if和try语句。

if (foo) {

    function x() {}

}

try {

    function x() {}

} catch(e) {

    console.log(e);

}

上面代码分别在if代码块和try代码块中声明了两个函数,按照语言规范,这是不合法的。但是,实际情况是各家浏览器往往并不报错,能够运行。

但是由于存在函数名的提升,所以在条件语句中声明函数,可能是无效的,这是非常容易出错的地方。

if (false) {

    function f() {}

}

f() // 不报错
上面代码的原始意图是不声明函数f,但是由于f的提升,导致if语句无效,所以上面的代码不会报错。要达到在条件语句中定义函数的目的,只有使用函数表达式。

if (false) {

    var f = function (){};

}

f() // undefined

6-1函数的属性和方法

name属性

name属性返回紧跟在function关键字之后的那个函数名。

function f1() {}
f1.name // 'f1'

var f2 = function () {};
f2.name // ''

var f3 = function myName() {};
f3.name // 'myName'
上面代码中,函数的name属性总是返回紧跟在function关键字之后的那个函数名。对于f2来说,返回空字符串,匿名函数的name属性总是为空字符串;对于f3来说,返回函数表达式的名字(真正的函数名还是f3,myName这个名字只在函数体内部可用)。

length属性

length属性返回函数预期传入的参数个数,即函数定义之中的参数个数。

function f(a, b) {}
f.length // 2
上面代码定义了空函数f,它的length属性就是定义时的参数个数。不管调用时输入了多少个参数,length属性始终等于2。

length属性提供了一种机制,判断定义时和调用时参数的差异,以便实现面向对象编程的”方法重载“(overload)。

toString()

函数的toString方法返回函数的源码。

function f() {

    a();

    b();

    c();

}

f.toString()
// function f() {

//  a();

//  b();

//  c();

// }
函数内部的注释也可以返回。

function f() {/*

 这是一个

 多行注释

 */}

f.toString()
// "function f(){/*

//   这是一个

//   多行注释

// */}"
利用这一点,可以变相实现多行字符串。

var multiline = function (fn) {

    var arr = fn.toString().split('\n');

    return arr.slice(1, arr.length- 1).join('\n');

};

function f() {/*

 这是一个

 多行注释

 */}

multiline(f);
// " 这是一个

//   多行注释"
 

 

7函数作用域

作用域(scope)指的是变量存在的范围。Javascript只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函数内部存在。

与全局作用域一样,函数作用域内部也会产生“变量提升”现象。var命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。

函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。

var a =1;
var x = function () {

    console.log(a);

};
function f() {

    var a = 2;

    x();

}
f() // 1
默认值
通过下面的方法,可以为函数的参数设置默认值。
function f(a){
  a = a || 1;
  return a;
}
 
f('') // 1
f(0) // 1
上面代码的||表示“或运算”,即如果a有值,则返回a,否则返回事先设定的默认值(上例为1)。
这种写法会对a进行一次布尔运算,只有为true时,才会返回a。可是,除了undefined以外,0、空字符、null等的布尔值也是false。也就是说,在上面的函数中,不能让a等于0或空字符串,否则在明明有参数的情况下,也会返回默认值。
为了避免这个问题,可以采用下面更精确的写法。
function f(a) {
  (a !== undefined&& a !== null) ? a = a : a = 1;
  return a;
}
 
f() // 1
f('') // ""
f(0) // 0
上面代码中,函数f的参数是空字符或0,都不会触发参数的默认值。

传递方式
函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。这意味着,在函数体内修改参数值,不会影响到函数外部。
var p = 2;
 
function f(p) {
  p = 3;
}
f(p);
 
p // 2
上面代码中,变量p是一个原始类型的值,传入函数f的方式是传值传递。因此,在函数内部,p的值是原始值的拷贝,无论怎么修改,都不会影响到原始值。
但是,如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。
var obj = {p: 1};
 
function f(o) {
  o.p = 2;
}
f(obj);
 
obj.p // 2
上面代码中,传入函数f的是参数对象obj的地址。因此,在函数内部修改obj的属性p,会影响到原始值。
注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。
var obj = [1, 2, 3];
 
function f(o){
  o = [2, 3, 4];
}
f(obj);
 
obj // [1, 2, 3]
上面代码中,在函数f内部,参数对象obj被整个替换成另一个值。这时不会影响到原始值。这是因为,形式参数(o)与实际参数obj存在一个赋值关系。
// 函数f内部
o = obj;
上面代码中,对o的修改都会反映在obj身上。但是,如果对o赋予一个新的值,就等于切断了o与obj的联系,导致此后的修改都不会影响到obj了。
某些情况下,如果需要对某个原始类型的变量,获取传址传递的效果,可以将它写成全局对象的属性。
var a = 1;
 
function f(p) {
  window[p] = 2;
}
f('a');
 
a // 2
上面代码中,变量a本来是传值传递,但是写成window对象的属性,就达到了传址传递的效果。
 

8参数

默认值
通过下面的方法,可以为函数的参数设置默认值。
function f(a){
  a = a || 1;
  return a;
}
 
f('') // 1
f(0) // 1
上面代码的||表示“或运算”,即如果a有值,则返回a,否则返回事先设定的默认值(上例为1)。
这种写法会对a进行一次布尔运算,只有为true时,才会返回a。可是,除了undefined以外,0、空字符、null等的布尔值也是false。也就是说,在上面的函数中,不能让a等于0或空字符串,否则在明明有参数的情况下,也会返回默认值。
为了避免这个问题,可以采用下面更精确的写法。
function f(a) {
  (a !== undefined&& a !== null) ? a = a : a = 1;
  return a;
}
 
f() // 1
f('') // ""
f(0) // 0
上面代码中,函数f的参数是空字符或0,都不会触发参数的默认值。

同名参数
如果有同名的参数,则取最后出现的那个值。
function f(a, a) {
  console.log(a);
}
 
f(1, 2) // 2
上面的函数f有两个参数,且参数名都是a。取值的时候,以后面的a为准。即使后面的a没有值或被省略,也是以其为准。
function f(a, a){
  console.log(a);
}
 
f(1) // undefined
调用函数f的时候,没有提供第二个参数,a的取值就变成了undefined。这时,如果要获得第一个a的值,可以使用arguments对象。
function f(a, a){
  console.log(arguments[0]);
}
 
f(1) // 1
 

9函数其他知识点

闭包
闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。
要理解闭包,首先必须理解变量作用域。前面提到,JavaScript有两种作用域:全局作用域和函数作用域。函数内部可以直接读取全局变量。
var n = 999;
 
function f1() {
  console.log(n);
}
f1() // 999
上面代码中,函数f1可以读取全局变量n。
但是,在函数外部无法读取函数内部声明的变量。
function f1() {
  var n = 999;
}
 
console.log(n)
// Uncaught ReferenceError: n is not defined(
上面代码中,函数f1内部声明的变量n,函数外是无法读取的。
如果出于种种原因,需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数。
function f1() {
  var n = 999;
  function f2(){
  console.log(n); // 999
  }
}
上面代码中,函数f2就在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是JavaScript语言特有的”链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
既然f2可以读取f1的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!
function f1() {
  var n = 999;
  function f2(){
    console.log(n);
  }
  return f2;
}
 
var result = f1();
result(); // 999
上面代码中,函数f1的返回值就是函数f2,由于f2可以读取f1的内部变量,所以就可以在外部获得f1的内部变量了。
闭包就是函数f2,即能够读取其他函数内部变量的函数。由于在JavaScript语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如f2记住了它诞生的环境f1,所以从f2可以得到f1的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。
function createIncrementor(start) {
  return function() {
    return start++;
  };
}
 
var inc = createIncrementor(5);
 
inc() // 5
inc() // 6
inc() // 7
上面代码中,start是函数createIncrementor的内部变量。通过闭包,start的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包inc使得函数createIncrementor的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。
为什么会这样呢?原因就在于inc始终在内存中,而inc的存在依赖于createIncrementor,因此也始终在内存中,不会在调用结束后,被垃圾回收机制回收。
闭包的另一个用处,是封装对象的私有属性和私有方法。
function Person(name) {
  var _age;
  function setAge(n){
    _age = n;
  }
  function getAge(){
    return _age;
  }
 
  return {
    name: name,
    getAge: getAge,
    setAge: setAge
  };
}
 
var p1 = Person('张三');
p1.setAge(25);
p1.getAge() // 25
上面代码中,函数Person的内部变量_age,通过闭包getAge和setAge,变成了返回对象p1的私有变量。
注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。

 

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: