JS中的函数

  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 属性,可以知道哪个函数被调用了,有利于找错。最后,如果一个函数要进行大量的计算,比如阶乘,可以给函数添加一个属性,把以前的计算结果保存起来,提高性能。

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就是call()或apply() 第一个参数,直接指定this值。

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;
}
  const any = new print();
  console.log(any);

  没有什么变化,返回的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
  当outer()函数调用完之后,按理说它里面的变量a就消失了,但返回的inner函数仍然可以引用a, 外部函数中的变量a并没有消失,内部函数把外部函数变量包起来,保留下来,这就是闭包。为什么呢?因为js是词法作用域,词法作用域有两个方面的意思:
  1,写代码的时候,变量的作用域就已经确定了。变量写在了什么地方,它的作用域就在什么地方。 在上面的例子中,变量b的作用域只在inner中,变量a 和 inner函数的作用域在outer函数中,
  2,变量的解析,当一个变量在它所在的作用域中,找不到时,它会向上,到包含作用域中查找,直到找到,或找到全局作用域,没有找到。比如b, 先在inner函数作用域中查找,找不到,再到包含inner 函数的outer函数作用域中查找,找到了,就不向上查找了。
  在我们书写代码的时候,创建了一个作用域链, inner函数不仅包含自己的作用域中的变量,还要包含函数中它所用到的变量。当函数在执行时候,它会按照既定的作用域链进行查找和解析,所以在返回inner 函数的时候,外部的a 不会消失,因为inner函数作用域中包含它。

  闭包在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

  new.target: 函数有两个内部方法, [[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.

   生成器函数:一个产生值的函数,不过它不是一次生成所有值,而是按需生成,调用一次,它生成一次。不同的地方在于,生成一个值以后,它不会停止,而是处于暂停状态,再次调用时,它会从以前的暂停的地方开始执行。生成器的函数声明要在function关键字后面加*,函数体内通过yield提供生成的值。

function* numberGenerator(){
    yield 1;
    yield 2;
    yield 3;
}

  生成器的调用并不会执行函数,而是会返回一个迭代器对象。迭代器有一个next方法,调用它,就会触发生成器的执行,生成器的执行呢,它碰到yield就暂停了,并把yield后面的值封装并返回。

const numberIterator = numberGenerator();
const result = numberIterator.next();

  再次调用next方法,生成器就会从上次停止的地方继续执行,碰到yield,它又停止了,并把后面值返回。

const result2 = numberIterator.next();

  可以打印result,它是一个对象,有value和done属性,value就是生成器生成的值,done表示生成器中还有没有值,如果是true,表示生成器中有值,如果是false,则表示生成器中没有值,同时value也被设置为undefined. 这样获取值有点麻烦,ES6提供了for of 方法

for (const num of numberGenerator()) {
    console.log(num)
}

  生成器可以遍历DOM

function* DomTraversal(element) {
            yield element;
            element = element.firstElementChild;
            while (element) {
                yield* DomTraversal(element);
                element = element.nextElementSibling;
            }
        }

  假设DOM 树

<div id="subTree">
    <form>
        <input type="text" />
    </form>
    <p>Paragraph</p>
    <span>Span</span>
</div>

  遍历结果

const subTree = document.getElementById("subTree");
for (let element of DomTraversal(subTree)) {
    assert(element !== null, element.nodeName);
}

  使用next方法给生成器传值,next方法可以接受参数。当调用next的时候,生成器把传递的过去的参数,替换掉它处于暂停状态时的yield表达式,并从暂停中恢复,继续执行

 

   next是给等待中的yield表达式传递值,如果没有等待中的yield表达式,也就没有值去传递,所以第一个调用next方法时,是没有办法传递值的。不过生成器和普通函数一样,可以接受参数。除了提供值以外,还可以给生成器抛出异常。每一个迭代器还有一个throw方法,它可以抛出异常给生成器。

 

 

  

 

posted @ 2020-07-07 15:16  SamWeb  阅读(352)  评论(0编辑  收藏  举报