继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

和Javascript函数谈一场恋爱(上)

慕九州0232256
关注TA
已关注
手记 7
粉丝 4
获赞 32


如果Javascript函数是我心中的那个女孩,那么,就让我们谈一场恋爱吧。

1 初见

初见Javascript函数,其实和其它语言的函数也没什么两样。

https://img.mukewang.com/5b5d08460001088308460694.jpg

1.1 函数的声明

function is_true() {
    return true;
}

用关键字function声明函数,有函数名,函数参数,函数体,一个标准的return关键字返回函数结果。

就连函数的调用也没什么不同:

console.log("call is_true", is_true());

输出结果:call is_true true

函数是一种类型:

console.log("is_true is", typeof is_true);

输出结果:is_true is function

函数是一个对象,在javascript的世界里,除了那几个调皮捣蛋,永远也长不大的基本数据类型以外,所有的东西都是对象。

让我们看看函数对象的名称属性:

console.log("is_true name is", is_true.name);

输出结果:is_true name is is_true

1.2 函数的参数

函数的参数非常简单:

function fun_param_0() {
    return 0;
}

console.log("fun_param_0 is", fun_param_0());

function fun_param_1(a) {
    return a;
}

console.log("fun_param_1 is", fun_param_1(1));

function fun_param_2(a, b) {
    return a + b;
}

console.log("fun_param_2 is", fun_param_2(1, 1));

输出结果:

fun_param_0 is 0
fun_param_1 is 1
fun_param_2 is 2

Javascript函数也支持可变参数:

unction fun_param_v() {
    var ret = 0;
    for (var i = 0; i < arguments.length; i++) {
        ret += arguments[i];
    }
    
    return ret;
}

console.log("fun_param_v is", fun_param_v(1, 2, 3, 4));

输出结果:fun_param_v is 10

arguments是函数对象自带的属性,函数刚声明的时候是空的,一旦函数被调用,这个属性就会被实参填满,如你所见,其实arguments就是一个数组。

其实函数声明时对于函数参数完全不用写:

function fun_param_2_not_param() {
    return arguments[0] + arguments[1];
}

console.log("fun_param_2_not_param is", fun_param_2_not_param(1, 1));

输出结果:fun_param_2_not_param is 2

这个函数和上面的fun_param_2函数是完全一致的,当然了,fun_param_2_not_param这种写法并不推荐使用,因为可读性不好。但是,你一定要记住,Javascript作为一门蹩脚的动态语言,函数的参数声明和实参个数不一致是非常容易的,这种口是心非可以看做是她的公主特权,我们必须限制她随意使用特权。

1.3 函数的返回值

函数的返回值非常简单:

function fun_return_0() {
    var a = 0;
}

console.log("fun_return_0 is", fun_return_0());

function fun_return_1() {
    return 1;
}

console.log("fun_return_1 is", fun_return_1());

function fun_return_v() {
    return { a : 1, b : 2 };
}

var r = fun_return_v();

console.log("fun_return_v is", fun_return_v(), r.a, r.b);

输出结果:

fun_return_0 is undefined
fun_return_1 is 1
fun_return_v is { a: 1, b: 2 } 1 2

如你所见,函数可以返回多个返回值,一般是通过对象的方式。

2 相识

有位美女做朋友是不是很有面子的一件事?关键是,你能不能一个电话就把美女约出来?

https://img4.mukewang.com/5b5d08f10001cf7105410692.jpg

2.1 男女平等

在Javascript的世界里,如果函数是女士,对象是男士,那么两者的社会地位是平等的。

本质上,函数就是对象,这是理解Javascript函数的第一准则。

因为函数是对象,所以它可以赋值给一个变量:

var is_false = function() {
    return false;
}

console.log("is_false is", typeof is_false);
console.log("is_false is", is_false.name);

输出结果:

is_false is function
is_false is is_false

习惯上,这被称为函数表达式。

因为函数是对象,所以它可以作为函数参数:

function is_true() {
    return true;
}

function is_false() {
    return false;
}

function is_boolean(fn) {
    console.log(fn());
}

is_boolean(is_true);
is_boolean(is_false);

输出结果:

true
false

因为函数是对象,所以它可以作为函数返回值:

function is_true() {
    return true;
}

function is_false() {
    return false;
}

function find_boolean(key) {
    if (key) {
        return is_true;
    }
    else {
        return is_false;
    }
}

console.log(find_boolean(true)());
console.log(find_boolean(false)());

输出结果:

true
false

所以,在Javascript的世界里,函数是一等公民。这其实是Javascript从出生就自带的基因,因为它的设计人就是搞函数式编程的,所以从那里借鉴了一点东西,没错,就是一点,也可以说Javascript是阉割版的函数式编程语言。

2.2 女士优先

在Javascript的世界里,如果函数是女士,则她拥有无可争辩的优先权,这就是女士优先的原则。

请看下面的代码:

    console.log("is_lady type is", typeof is_lady);
    
    function is_lady() {
    }
    
    var is_lady = 4;

输出结果:is_lady type is function

这段很简单的代码从其它编程语言的视角来看问题多多:

  • 第一行,is_lady还未被声明,能不能用?

  • is_lady先被声明为函数,又被声明为变量,哪个优先?

这段代码在Javascript的世界里完全合法,这是由这门语言的本性决定的。

首先,Javascript是一门编译语言,它的正确运行要分两步:先编译,后执行。在编译阶段,编译器会检查所有的函数声明和变量声明,而根本不关心函数是怎么定义的,变量是怎么赋值的。在编译器的眼中,这段代码实际上是下面的样子:

    function is_lady() {
    }
    
    var is_lady;

    console.log(typeof is_lady);
    
    is_lady = 4;

如你所见,函数声明和变量声明都被提升了。

其次,Javascript是一门动态语言,它允许一个名称先赋予函数再赋予变量或者反过来。那么在变量声明和函数声明之间哪个优先级更高呢?Javascript规定:函数声明的优先级更高。

特别的,函数还可以多次声明,从而导致了一个覆盖的问题:

    is_lady();
    
    function is_lady() {
        console.log("cover", 1);
    }
    
    function is_lady() {
        console.log("cover", 2);
    }
    
    var is_lady = function() {
        console.log("cover", 3);
    }

输出结果:cover 3

仔细分析一下上面的代码:第二个is_lady覆盖了第一个is_lady,而第三个is_lady又因为是变量,没有前两个is_lady优先级高。

你看到了吧,在Javascript代码中一个作用域内用同样的名字多么恐怖的一件事,这也变相的鼓励我们在代码中最好用不同的名字。

函数优先的原则不仅表现在编译器提升上,更重要的表现其实是函数的嵌套:

    function outer() {
        console.log(1);
    
        function inner() {
            console.log(2);
        }
        
        inner();
        
        console.log(3);
    }
    
    outer();

输出结果:

1
2
3

函数里面还可以有函数,这在其它语言中是很难想象的,但是在Javascript的世界里,函数基本上是想去哪就去哪。其实这也没啥好奇怪的,别忘了我们的第一准则,然后看看下面的代码:

var outer = {
    a : 1,
    inner : {
        b : 2,
    },
    c : 3,
};

console.log(outer);

输出结果:{ a: 1, inner: { b: 2 }, c: 3 }

函数中的函数就没啥不好理解的了。

2.3 立即执行

在Javascript的世界里,如果函数是女士,你就得理解她间歇式冲动爆发的行为。

函数声明和函数调用的分离有时候并不是我们期望的,Javascript还支持函数声明和函数调用绑在一起的功能,这通常被称为立即执行函数表达式。

(function buy() {
    console.log("买,买,买。。。");
}());

(function buy() {
    console.log("买,买,买。。。");
})();

void function buy() {
    console.log("买,买,买。。。");
}();

!function buy() {
    console.log("买,买,买。。。");
}();

输出结果:

买,买,买。。。
买,买,买。。。
买,买,买。。。
买,买,买。。。

看着这一堆的买,买,买,是不是特别有一种剁手的冲动:)

因为都是立即执行函数表达式,所以函数名本身其实并不重要,但是根据Javascript大佬们的建议,最好所有的函数都给个名称。

从语法上来说,下面的代码是编译不过去的:

function buy() {
    console.log("买,买,买。。。");
}();

因为编译器很难区别函数声明中的()和函数调用中的()。所以通常的做法就是用操作符来改变一下表达式的优先级。

  1. 第一种,用()把所有东西都括起来是可以的

  2. 第二种,用()把函数声明括起来也是可以的(用()把后面的()括起来就不要想了吧)

  3. 第三种,用void运算符

  4. 第四种,用!运算符

其实用“+”,“-”,“~”之类的无副作用的单目运算符也是可以的,就是看起来怪怪的,还是推荐用()的方式。

2.4 谁的电话

在Javascript的世界里,如果函数是女士,谁来约她是很关键的。比如谈到她的酒量,如果你是一位帅哥,那个她一杯就倒,如果你是一个loser,那么她千杯不醉。

在其它面向对象的语言中,习惯于将类的成员函数第一个参数定义为this引用(或指针),以标记是哪个对象调用了该函数。Javascript也借鉴了这一点。我们可以在函数中使用this。

    function is_this() {
        return this;
    }
    
    console.log("this is", is_this());

输出结果(浏览器中节选):

https://img4.mukewang.com/5b5cfaa5000168af01530051.jpg

输出结果(node.js中节选):

https://img1.mukewang.com/5b5cfafc00012b1c04020227.jpg

这里的this返回的实际上是全局对象。

函数的中的this引用,网上相关的文档比比皆是,但只要想到我们的第一准则,其实基本规则非常简单:

  1. 如果有明显的调用对象,那么this就是这个对象

  2. 没有没有明显的调用对象,那么this就是全局对象

基本上,函数调用可以分为四种情况,上面已经演示了第一种,作为函数进行调用,因为没有明显的调用对象,所以this就是全局对象。

第二种,作为方法进行调用:

    var i = {
        call : function() {
            console.log("一起看电影啊");
            return this;
        }
    };
    console.log("obj call is", typeof i.call(), i.call());

输出结果:

一起看电影啊
一起看电影啊
obj call is object { call: [Function: call] }

这就是有明显的调用对象,所以this就是这个对象。

第三种,作为构造器进行调用:

    function Person(name, age) {
        this.name = name;
        this.age = age;
        console.log("new this is", typeof this, this);
    }
    var p = new Person("张三", 25);
    console.log("name:", p.name, "age:", p.age);
    var q = new Person("李丽", 20);
    console.log("name:", q.name, "age:", q.age);
    console.log("name:", p.name, "age:", p.age);

输出结果:

new this is object Person { name: '张三', age: 25 }
name: 张三 age: 25
new this is object Person { name: '李丽', age: 20 }
name: 李丽 age: 20
name: 张三 age: 25

只要函数和new搞在了一起,那就叫构造器。new的特点就是每次都生成新的对象,所以这里的p,q实际上引用的是不同的对象。

这里有明显的调用对象,所以this就是这个对象。

第四种,作为指定进行调用:

    function is_this() {
        return this;
    }
    
    console.log(is_this.call({}));
    console.log(is_this.call(window));
    console.log(is_this.call(null));
    console.log(is_this.call(undefined));

输出结果:

https://img2.mukewang.com/5b5d077e000158fc04940083.jpg

call函数允许用户自定义this,你指定那个对象,this就是那个对象,如果找不到,那就是全局对象。

apply函数与此类似,只不过函数参数必须用数组而已。

未完待续。

打开App,阅读手记
4人推荐
发表评论
随时随地看视频慕课网APP