JS中的函数是一等公民,也就是说,它和其它对象或值地位相同,没有区别,其它对象或值怎么用,函数就可以怎么用。其他对象或值怎么用呢?以对象为例,它可以通过字面量进行创建,可以赋值一个变量,可以做为参数传递给函数,同时也可以被函数返回,最后,它的属性还可以动态创建和赋值。
({name: 'sam'}) // 通过对象字面量创建 const obj = { name: 'sam'}; // 赋值给一个变量 fetch('url', obj); // 作为参数 function returnObj() { // 作为返回值 return { name: 'sam'}; } obj.job = "web" // 动态创建属性
函数可以做同样的事情
function add(a, b) { // 函数字面量 return a + b; } let substract = (a, b) => a - b; // 函数赋值给变量 [1, 2, 3].sort((a, b) => a - b); // 函数做为参数进行传递 function curry(cb) { // 返回一个函数 return cb; } add.id = 'sum'; // 给函数动态创建一个属性并赋值
函数就是对象(一个值),对象能做什么,函数就能做什么,它拥有对象的一切能力,函数可以被看成对象。函数作为对象,给它添加属性,可以实现有趣的功能,比如给它加一个id属性,可以确保函数的唯一,不会重复地向一个对象中添加相同的函数,再比如函数有一个name 属性,可以知道哪个函数被调用了,有利于debug。最后,如果一个函数要进行大量的计算,比如阶乘,可以给函数添加一个属性,把以前的计算结果保存起来,提高性能。
function factoral(n) { if(!factoral.result) { // result 保存计算结果 factoral.result = {}; } if (factoral.result[n] !== undefined) { // 如果以前计算过,直接返回结果 return factoral.result[n]; } if (n === 1) { return 1; } else { return factoral.result[n] = n * factoral(n - 1); } } console.log(factoral(5));
当然,函数除了是对象之外,还有一个重要的特点,就是它可以被调用,实现某些功能,它是一个可以被调用的对象。但函数的调用在JS中也比较复杂,有四种不同方式,
1,作为函数调用,就是最普通的调用方式,函数名加上(), 有可能还要加上参数。比如:Number("2")
2,作为对象的属性调用,有的也称为方法调用。对象的属性是一个函数,就可以使用对象.属性进行调用. console.log('hello');
3,作为构造函数调用,使用new 加上函数名。new Promise(function(){})
4,通过函数拥有的方法call() 和apply() 调用. [].slice.call();
为什么有这么多的调用方式呢?不就是调用一个函数吗?原因在于函数中有一个隐式的this参数,不管你用不用,this 存在每一个函数中。this 呢,又比较特别,只有在函数的调用的时候,才能知道它是什么,四种不同的调用方式,就是四种不同的this 值。
作为函数调用,在非严格模式下,this的值是全局对象global, 具体到浏览器中,是window 对象。
function sayThis() {
console.log(this === window);
}
sayThis();
在严格模式下,this的值是undefined。严格模式,就是整个js文件或函数体中使用""use strict";
function sayThis() { "use strict"; console.log(this === undefined); } sayThis();
作为对象的属性调用, this的值是拥有这个属性的对象。对象的属性值可以是一个函数。
function sayThis() { "use strict"; console.log(this === obj); } const obj = { sayThis:sayThis } obj.sayThis();
作为构造函数调用,就是调用函数的时候前面加上new, 它创建了全新的对象,this 指向这个对象
function sayThis() { "use strict"; console.log(this === {}); } new sayThis();//{}
call() 和apply(), 他们第一个参数就是this, 函数调用的时候,直接指定this. 函数中的this就等行call()或apply() 第一个对数。
function sayThis() { "use strict"; console.log(this === obj); } const obj = { name: 'sam' } sayThis.call(obj); // obj
如果函数中没有this, 使用最普通的调用方式就可以了,没有必要使用复杂的调用了。没有this,函数就没有运行时要决定的变量this,函数怎么写的,调用的时候就怎么执行,使用哪种方式调用,结果都是一致的。解释一下作为构造函数的调用。在JS中,没有构造函数一说,有的只是普通的函数。函数(箭头函数除外)前面都可以加上new 时进行调用。
function print() { console.log('hello'); } const any = new print(); // hello console.log(any); // {}
使用new调用函数时,它先创建了一个对象,如果函数中有this, 就把对象赋值给this, 然后执行函数体。如果没有this 呢,就是执行函数体,执行完函数体后,就把对象返回。new的调用,就是创建对象,执行函数体,返回对象。那如果我们函数中直接返回一个值呢,比如返回一个1,使用new 调用会怎么样?
function print() { console.log('hello'); return 1; }
没有什么变化,返回的1被舍弃了,使用new调用返回原始类型的值的函数,这个原始值会被舍弃,new调用返回的还是new创建的对象。如果new调用的是返回对象的函数呢?
function print() { console.log('hello'); return [1, 2]; } const any = new print(); console.log(any); // [1,2]
new调用返回的是函数的返回值,new创建的对象被舍弃了。JS中函数,绝大多数都可以在调用的时候,前面加上new, 但函数样式不同,返回的值也不同,所以如果真的要让函数作为构造函数进行使用,就要遵循一定的规范,函数中使用this, 不要有返回值,使用默认返回值,函数名最好首字母大写。
function Person(name, job) { this.name = name; this.job = job; } const person = new Person('sam', 'web'); console.log(person);
再说一下方法调用的一个问题,this的丢失。
const obj = { name: 'sam', sayName() { return this.name } } const anotherfun = obj.sayName; console.log(anotherfun()); // undefined
obj 对象有一个sayName() 方法,简单地返回对象的name. 把obj.sayName 赋值给另一个变量,然后进行调用,可以发现并没有返回对象的name值。为什么呢?obj的sayName 属性,它并不是真正拥有函数,而是一个引用,指向函数。当我把obj.sayName赋值给一个变量的时候,赋值的也是引用,也就是说anotherfun 也指向了obj.sayName 指向的函数,相当于
const anotherfun = function(){ return this.name };
anotherfun函数进行调用的时候,也是最普通的调用方法,加(), 所以函数中的this指向了window,antherfun返回的值是window 对象中的name. 这也印证了,只有在调用的时候,才能决定this是什么。要想快速的知道this 具体的指向,就要准确的定位到函数是什么地方调用的,函数的调用点。 解决这个问题的办法,使用bind()。函数有一个bind方法,接受的第一个参数就是this, 用来指定函数中的this,返回一个函数,那么返回的函数中, this是固定的。再赋值给其它变量时,里面的this 就不会动态变化了。
const anotherfun = obj.sayName.bind(obj); console.log(anotherfun()); // 'sam'
this的丢失还有一种情况,对象的属性值是一个包含函数的函数,内部的函数中的this并不会继承外部函数中的this
const obj = { name: 'sam', sayName() { (function() { console.log(this) // window console.log(this.name) })() } } obj.sayName();
内部的函数调用也相当于函数的普通调用,this指向了window. 解决这个问题的办法是箭头函数。箭头函数没有自己的this, 它内部的this继承自外围作用域,并且this的值(指向)是在它定义的时候,就已经确定了,就像使用了bind方法,而不是使用动态绑定。箭头函数就是 参数列表 => 函数体;如(a,b) => a+b; 它是一个匿名函数表达式,要把它赋值给一个变量引用,才能对它进行调用
let sum = (a,b) => a+b; sum(1, 2)
简单解释一下,箭头函数接受两个参数a,b 返回 a + b的值。函数体如果是一句表达式,默认会返回表达式的值,这也是没有写return a +b 的原因,这里要注意一点,如果返回一个对象,这个对象要用() 括起来。
let obj = name =>({name:name}) // 如果不写外面的括号,{} 就会被当做块级作用域
箭头函数还有其他变体
let hello = () => console.log('hello'); // 箭头函数没有参数,直接用一个括号表示 let add10 = num => num +10; // 箭头函数只有一个参数num,通常直接写这个参数,不用括号括起来。 // 函数体是一段可以执行的语名块, 需要用{}把语名块包起来,如果语句块执行完毕,还要返回值,那就要在语句块的末尾显示调用return let amount = n => { let sum =0; for(let i=0; i<=n; i++){ sum = sum + i; } return sum; }
箭头函数没有自己的this,但可以在它里面使用this,这时this的指向就继承自外围作用域。this 存在两个地方,一个是函数中,一个是全局对象window。继承自外围作用域就是说箭头函数中的this使用的要么是它的父函数或祖先函数中的this,要么是window对象。this会顺着函数的作用域链向上进行查找,直到找到包含它的一个函数,然后使用该函数中的this,如果找不到,那就是window对象。举个例子
const object = { f1: function(){ console.log(this); const f2 = () => console.log(this);
f2(); } } object.f1(); // f1 函数内部的this全都指向 object
f2箭头函数的this,向上找,找到了包含它的函数f1,那就使用f1中的this,f2 中的this 和f1 中的this 保持一致。再改一下,把f1 也改成箭头函数,
const object = { f1: () => { console.log(this); var f2 = () => console.log(this); f2(); setTimeout(f2, 1000); } } object.f1(); // f1 函数内部的this全都指向window
this 指向了window, 按照 object.f1() 的调用方式,f1 函数中的this 应该指向object.其实不是,箭头函数的this 是在它定义的时候,就已经确定了,就像使用了bind方法,而不是动态绑定了, 当箭头函数调用的时候,真正要确定的是它在定义的时候,它所能向上寻找到的包含它的最外围的函数中this. 我们再来分析一下,f2 向上找f1, f1 也是箭头函数,它还要向上找,但你发现包含f1的函数没有了,只有全局对象window了,this 指向了window, 箭头函数在调用的时候,它真正确定的是包含f1函数的函数中this 的指向, 如果没有包含函数,就是window全局对象了。用箭头函数来解决this 丢失的问题,就是函数的属性值是普通函数,属性值函数中的所有函数都用箭头函数,普通函数包含箭头函数
const obj = { name: 'sam', sayName() { (() => { console.log(this) // window console.log(this.name) })() } } obj.sayName();
函数调用的时候,如果返回一个函数,那就有可能涉及到另外一个问题----闭包。闭包,最常见的就是一个函数包含另外一个函数,内部的函数可以访问外部函数中的变量,纵然外部函数消失了。举例
function outer() { let a = 10; function inner() { let b = 20; console.log(a + b); } return inner; } let inner = outer(); inner(); // 30
闭包在JS中是天然存在的,因为JS中的函数是值,可以包含在另外一个函数中,也可以被返回。再者,JS是词法作用域
ES6函数增强
声明函数的时候,给形参赋一个值,这个值就是参数的默认值。调用函数时,如果没有进行相应的实参传递,参数就会使用默认值。
// num2拥有默认参数值5,如果没有给num2形参传值,它的取值将会是5
function sum (num1, num2 = 5) {
return num1 + num2;
}
console.log(sum(1)) // 6 调用sum函数时, 只传递了一个参数1,所以函数中num1 =1, num2就会使用默认参数值5, 1+5 =6;
console.log(sum(1,2)) // 3 函数调用时,我们传递了两个参数,所以默认参数值不起作用, 函数使用我们传递过去的参数 1+2 =3
这里所说的‘值’是广义的值,不仅仅是指像5这样的简单值,它可以是任意的js表达式,甚至是函数的调用
function getValue(value) {
return value + 5;
}
// 函数参数是第一个参数的值。
function add(first, second = getValue(first)) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 7
正如你看到的那样,默认参数为函数时,这个函数的调用是惰性的,如add(1,1)传递了两个参数,函数就不会调用。add(1)只传递一个参数, 这个函数才会调用。其次,getValue函数可以把函数的第一个参数first 作为自己的参数。但是这里有一个小细节要注意,函数的形参也有了自己的作用域,形参的作用域只是把函数声明()中的参数包起来,如果在参数默认值中去解析一变量,它先从形参作用域中进行查找,如果没有找到,它再从函数外面的作用域中进行查找。add 函数的参数的声明就像下面一样
let first; let second = getValue(first);·
getValue中的first参数,正好在形参作用域中找到了,所以没有问题,如果写反了,function add( second = getValue(first), first) {}, 函数的参数声明就变成了
let second = getValue(first); let first;
first 变量还没有声明,就使用了,造成了暂存死区。再来看一个例子,
let w = 1, z = 2;
function foo( x = w + 1, y = x + 1, z = z + 1 ) {
console.log( x, y, z );
}
foo(); // ReferenceError
先执行w + 1, 就会从形参作用域中找w,没有找到,就到函数外面去找,正好找到了w=1, 那x就赋值为2,再执行y=x + 1, 这时从形参作用域中找到了x,y变成了3,最后是z = z + 1,先执行z+1,在形参作用域找到了z,它就不会从外面的作用域去找了,但是z声明在后,z+1 就变成了引用一个未声明的变量,造成了暂存死区
当参数拥有默认值以后,它影响了argumets 对象。我们都知道,每一个函数内部都有一个arguments 对象,保存函数调用时传递过去的参数,第一个参数对应的就是arguments[0], 第二个参数对应的就是arguments[1]. 像上面的sum 函数, num1 == argument[0]; 但有了默认参数值,这种对于关系打破了. sum(1) 调用sum 函数的时候,我们只传递了一个值1,也就意味着arguments[1] 的值是undefined, 但是它对应的num2 形参,num2 参数由于默认值的存在,这里取5. arguments[1] 就不等于num2 了。还有一点就是,arguments 只是保存了传递过去的值,如果在函数内部 参数的值有更改,那么arguments 也不会实时反应这种变化,还是上面的sum(1) 调用,arguments[0] 永远等于1。 初始的时候,num1 == arguments[0]; 但如果在函数体中 num1 重新赋值为2, arguments[0] 就不等于num1 了。
function sum (num1, num2 = 5) {
console.log(arguments.length); // 1, 只传递了一个参数
console.log(num1 === arguments[0]); // true 初始时相等
console.log(num2 === arguments[1]); // false 只传一个参数,arguments[1] 是undefined, num2 取默认值5
num1 = 2;
console.log(num1 === arguments[0]); // false arguments只保存调用时的初值。
return num1 + num2;
}
console.log(sum(1));
记住一点就可以了, arguments 对象只保存调用函数时传递过去的参数的初始值。不太理解也没有关系,arguments 对象几乎用不到了,因为ES6 提供了更好的参数保存方式(剩余参数rest),下面会介绍。
剩余参数 (rest)
当我们调用函数的时候,我们可以传递任意数量的实参给函数,如果函数形参的数量少于实参的数量,我们就只能通过函数内部的arguments 获取多余的实参。ES6 提供了一个更简单的方法来获取这些多余的参数,就是剩余参数。我们在声明函数的时候,在一个参数的前面加上..., 这个参数就变成了一个数组,它会把多余的参数收集到它里面,变成它的元素。
let sum = (obj, ...rest) => {
console.log(rest) // [2,3,4,5]
}
sum({a:1},2,3,4,5)
上面代码中的rest就是一个剩余参数,它把2,3,4,5 收集起来,变成了它的元素,它本身是一个数组。
注意:一个函数中只能有一个剩余参数,且它必须放到所有参数最后,这很好理解,因为,它把所有参数都收集到一起了,一个就足够了,如果它后面还有参数,这些参数也获取不到数据了,所以也就没有必要设置参数了。
扩展操作符(...)
扩展操作符,把一个可迭代对象(如数组)扩展给一个一个的单体。
let array = [1,2,3,4,5];
console.log(...array) // 1 2 3 4 5
js 中的函数有两个内部的方法, [[Call]] 和[[Construct]] , 当我们调用函数的时候,没有使用new, 那[[Call]] 方法就会被调用,执行函数体。当调用函数的时候前面加了一个new, 那[[Construct]] 方法就会被调用,生成一个对象,调用函数,给对象赋值,并返回对象。当然并不是每一个函数都有[[Construct]] 方法,比如箭头函数就没有,所以箭头函数就不能使用new 进行调用。那怎么决定函数是用那种方法呢?
ES6 增加了一个new.target, 如果一个函数通过new 调用,它内部会获取一个new.target 的元属性,它指向的就是我们的构造函数。 当然,如果这个函错误地通过一般函数调用,new.target 就是undefined. 这样我们就可以轻松地判断一个函数是不是通过new进行调用,从而避免了构造函数用普通方式进行调用产生的错误。
function Person(name) {
if (typeof new.target !== "undefined") {
this.name = name;
} else {
console.error("You must use new with Person.")
}
}
var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // You must use new with Person.