资讯专栏INFORMATION COLUMN

深入理解ES6笔记(三)函数

aristark / 1981人阅读

摘要:主要知识点有函数参数默认值剩余参数扩展运算符属性块级函数箭头函数以及尾调用优化深入理解笔记目录函数的默认参数在中,我们给函数传参数,然后在函数体内设置默认值,如下面这种方式。拥有方法的函数被称为构造器。

主要知识点有:函数参数默认值、剩余参数、扩展运算符、new.target属性、块级函数、箭头函数以及尾调用优化

《深入理解ES6》笔记 目录

函数的默认参数

在ES5中,我们给函数传参数,然后在函数体内设置默认值,如下面这种方式。

function a(num, callback) {
  num = num || 6
  callback = callback || function (data) {console.log("ES5: ", data)}
  callback(num * num)
}
a() //ES5: 36,不传参输出默认值

//你还可以这样使用callback
a(10, function(data) {
  console.log(data * 10) // 1000, 传参输出新数值
})

弊端:此处的 num 的有效值实际上有可能是 0 ,但因为 0 是假值,就会导致 num 的值在这种情况下会被替换为 6;
可以用 typeof 来检测参数的类型:

function a(num, callback) {
  num = (typeof num!== "undefined") ? num: 6;
  callback = (typeof callback !== "undefined") ? callback : function (data) {console.log("ES5: ", data)};
  callback(num * num)
}

虽然这种方法更安全,但依然为实现一个基本需求而书写了过多的代码。它代表了一种公共
模式,而流行的 JS 库中都充斥着类似的模式。

ES6 中的参数默认值
function a(num = 6, callback = function (data) {console.log("ES6: ", data)}) {
  callback(num * num)
}

a() //ES6: 36, 不传参输出默认值

a(10, function(data) {
  console.log(data * 10) // 1000,传参输出新数值
})

使用ES6的默认值写法可以让函数体内部的代码更加简洁优雅

参数默认值如何影响 arguments 对象

ES5 的非严格模式下

function mixArgs(first, second) {
    console.log(first === arguments[0]);
    console.log(second === arguments[1]);
    first = "c";
    second = "d";
    console.log(first === arguments[0]);
    console.log(second === arguments[1]);
}
mixArgs("a", "b");
//输出
true
true
true
true

ES5 的严格模式下

function mixArgs(first, second) {
    "use strict";
    console.log(first === arguments[0]);
    console.log(second === arguments[1]);
    first = "c";
    second = "d"
    console.log(first === arguments[0]);
    console.log(second === arguments[1]);
}
mixArgs("a", "b");
//输出
true
true
false
false

ES6

arguments 对象的表现总是会与 ES5 的严格模式一致,无论此时函数是否明确运行在严格模式下。

// 非严格模式
function mixArgs(first, second = "b") {
    console.log(arguments.length);
    console.log(first === arguments[0]);
    console.log(second === arguments[1]);
    first = "c";
    second = "d"
    console.log(first === arguments[0]);
    console.log(second === arguments[1]);
}
mixArgs("a");
//输出
1
true
false
false
false

此时arguments.length =1 ,因为只给 mixArgs() 传递了一个参数。这也意味着arguments[1] 的值是 undefined ,符合将单个参数传递给函数时的预期;这同时意味着first 与 arguments[0] 是相等的。改变 first 和 second 的值不会对 arguments 对象造成影响,无论是否在严格模式下,所以你可以始终依据 arguments 对象来反映初始调用状态。

默认参数表达式

参数不仅可以设置默认值为字符串,数字,数组或者对象,还可以是一个函数。

function add() {
  return 10
}
function a(num = add()){
  console.log(num)
}
a() // 10
默认参数的临时死区

第一章我们提到了let和const什么变量的临时死区(TDZ),默认参数既然是参数,那么也同样有临时死区,函数的作用域是独立的,a函数不能共享b函数的作用域参数。

//这是个默认参数临时死区的例子,当初始化a时,b还没有声明,所以第一个参数对b来说就是临时死区。
function add(a = b, b){
  console.log(a + b)
}
add(undefined, 2) // b is not define
处理无命名参数

上面说的参数都是命名参数,而无命名参数也是函数传参时经常用到的。当传入的参数是一个对象,不是一个具体的参数名,则是无命名参数。

function add(object){
  console.log(object.a + object.b)
}
let obj = {
  a: 1,
  b: 2
}
add(obj) // 3
不定参数

使用...(展开运算符)的参数就是不定参数,它表示一个数组。

function add(...arr){
  console.log(a + b)
}
let a = 1,b = 2
add(a, b) // 3

不定参数的使用限制:

函数只能有一个剩余参数,并且它必须被放在最后

//错误的写法1
function add(...arr, c){
  console.log(a + b)
}
let a = 1,b = 2,c = 3
add(a, b, c)

剩余参数不能在对象字面量的 setter 属性中使用

//错误的写法2
let obj = {
  set add(...arr) {
  
  }
}

剩余参数如何影响 arguments 对象
arguments 对象在函数被调用时反映了传入的参数,与剩余参数能协同工作,就像如下程序所演示的:

function checkArgs(...args) {
    console.log(args.length);
    console.log(arguments.length);
    console.log(args[0], arguments[0]);
    console.log(args[1], arguments[1]);
}
checkArgs("a", "b");
//输出
2
2
a a
b b

arguments 对象总能正确反映被传入函数的参数,而无视剩余参数的使用。

ES6中的构造函数Function新增了支持默认参数和不定参数。

扩展运算符

考虑一下Math.max()方法,它接受任意数量的参数,并会返回其中的最大值。

//两个值进行比较
let value1 = 25,
value2 = 50;
console.log(Math.max(value1, value2)); // 50
//一个数组中找到最大值(es5)
let values = [25, 50, 75, 100]
console.log(Math.max.apply(Math, values)); 
//es6
let values = [25, 50, 75, 100]
// 等价于 console.log(Math.max(25, 50, 75, 100));
console.log(Math.max(...values)); // 100

扩展运算符传递参数

//假设你想让  Math.max()  返回的最小值为 0 (以防数组中混入了负值),你可以将参数 0 多带带传入,并继续为其他参数使用扩展运算符
let values = [-25, -50, -75, -100]
console.log(Math.max(...values, 0)); // 0
ES6 的名称属性

ES6 给所有函数添加了 name 属性。

选择合适的名称
//函数声明
function doSomething() {
// ...
}
//匿名函数表达式
var doAnotherThing = function() {
// ...
};
console.log(doSomething.name); // "doSomething"
console.log(doAnotherThing.name); // "doAnotherThing"
名称属性的特殊情况
//doSomethingElse的优先级高于doSomething 
var doSomething = function doSomethingElse() {
// ...
};
//person.firstName  实际是个 getter 函数,因此它的名称是  "get firstName"
var person = {
    get firstName() {
        return "Nicholas"
    },
    sayName: function() {
        console.log(this.name);
    }
}
console.log(doSomething.name); // "doSomethingElse"
console.log(person.sayName.name); // "sayName"
var descriptor = Object.getOwnPropertyDescriptor(person, "firstName");
console.log(descriptor.get.name); // "get firstName"
另外两个特殊情况

使用 bind() 创建的函数会在名称属性值之前带有"bound"前缀

使用 Function 构造器创建的函数,其名称属性则会有 "anonymous" 前缀

var doSomething = function() {
// ...
};
console.log(doSomething.bind().name); // "bound doSomething"
console.log((new Function()).name); // "anonymous"
明确函数的双重用途

JS 为函数提供了两个不同的内部方法: [[Call]] 与 [[Construct]] 。当函数未使用 new进行调用时, [[call]] 方法会被执行,运行的是代码中显示的函数体。而当函数使用 new进行调用时, [[Construct]] 方法则会被执行,负责创建一个被称为新目标的新的对象,并
且使用该新目标作为 this 去执行函数体。拥有 [[Construct]] 方法的函数被称为构造器。

在 ES5 中判断函数如何被调用

使用instanceof

function Person(name) {
    if (this instanceof Person) {
        this.name = name; // 使用 new
    } else {
        throw new Error("You must use new with Person.")
    }
}
var person = new Person("Nicholas");
var notAPerson = Person("Nicholas"); // 抛出错误

但这种情况下并不可靠:

function Person(name) {
    if (this instanceof Person) {
        this.name = name; // 使用 new
    } else {
        throw new Error("You must use new with Person.")
    }
}
var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // 奏效了!
new.target 元属性

通过检查 new.target 是否被定义,这个新的元属性就让你能安全地判断函数是否被使用new进行了调用。

function Person(name) {
    if (typeof new.target !== "undefined") {
        this.name = name; // 使用 new
    } else {
        throw new Error("You must use new with Person.")
    }
}
var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // 出错!

也可以检查 new.target 是否被使用特定构造器进行了调用,例如以下代码:

function Person(name) {
    if (new.target === Person) {
        this.name = name; // 使用 new
    } else {
        throw new Error("You must use new with Person.")
    }
}
function AnotherPerson(name) {
    Person.call(this, name);
}
var person = new Person("Nicholas");
var anotherPerson = new AnotherPerson("Nicholas"); // 出错!
警告:在函数之外使用  new.target  会有语法错误。
块级函数 严格模式的块级函数
"use strict";
if (true) {
    // 在 ES5 会抛出语法错误, ES6 则不会
    function doSomething() {
    // ...
    }
}

块级函数会被提升到定义所在的代码块的顶部:

"use strict";
if (true) {
    console.log(typeof doSomething); // "function"
    function doSomething() {
    // ...
}
doSomething();
}
console.log(typeof doSomething); // "undefined"

let 函数表达式:

"use strict";
if (true) {
    console.log(typeof doSomething); // 抛出错误
    let doSomething = function () {
        // ...
    }
doSomething();
}
console.log(typeof doSomething);
非严格模式的块级函数

ES6 在非严格模式下同样允许使用块级函数,但行为有细微不同。块级函数的作用域会被提升到所在函数或全局环境的顶部,而不是代码块的顶部。

// ES6 behavior
if (true) {
    console.log(typeof doSomething); // "function"
    function doSomething() {
    // ...
    }
doSomething();
}
console.log(typeof doSomething); // "function"
箭头函数

箭头函数与传统的 JS 函数区别:

没有 this 、 super 、 arguments ,也没有 new.target 绑定

不能被使用 new 调用

没有原型: 既然不能对箭头函数使用 new ,那么它也不需要原型,也就是没有prototype 属性。

不能更改 this : this 的值在函数内部不能被修改,在函数的整个生命周期内其值会保持不变

没有 arguments 对象

不允许重复的具名参数

箭头函数语法

无参数

var getName = () => "Nicholas";
// 有效等价于:
var getName = function() {
    return "Nicholas";
};

单个参数

var reflect = value => value;
// 有效等价于:
var reflect = function(value) {
    return value;
};

多个参数

var sum = (num1, num2) => num1 + num2;
// 有效等价于:
var sum = function(num1, num2) {
    return num1 + num2;
};

多个函数语句体

var sum = (num1, num2) => {
    return num1 + num2;
};
// 有效等价于:
var sum = function(num1, num2) {
    return num1 + num2;
};

//将对象字面量包裹在括号内,标示了括号内是一个字面量而不是函数体。
var getTempItem = id => ({ id: id, name: "Temp" });
// 有效等价于:
var getTempItem = function(id) {
    return {
        id: id,
        name: "Temp"
    };
};
创建立即调用函数表达式

传统函数

let person = function(name) {
    return {
        getName: function() {
            return name;
        }
    };
}("Nicholas");
console.log(person.getName()); // "Nicholas"

箭头函数

let person = ((name) => {
    return {
    getName: function() {
        return name;
    }
};
})("Nicholas");
console.log(person.getName()); // "Nicholas"
译注:使用传统函数时,  (function(){/函数体/})();  与  (function(){/函数体/}());
这两种方式都是可行的。
但若使用箭头函数,则只有下面的写法是有效的: (() => {/函数体/})();
尾调用优化

尾调用是指在函数return的时候调用一个新的函数,由于尾调用的实现需要存储到内存中,在一个循环体中,如果存在函数的尾调用,你的内存可能爆满或溢出。

ES6中,引擎会帮你做好尾调用的优化工作,你不需要自己优化,但需要满足下面3个要求:

1、函数不是闭包

2、尾调用是函数最后一条语句

3、尾调用结果作为函数返回

一个满足以上要求的函数如下所示:

"use strict";   
function a() {
  return b();
}

下面的都是不满足的写法:

//没有return不优化
"use strict";
function a() {
  b();
}

//不是直接返回函数不优化
"use strict";
function a() {
  return 1 + b();
}

//尾调用是函数不是最后一条语句不优化
"use strict";
function a() {
  const s = b();
  return s
}

//闭包不优化
"use strict";
function a() {
  const num = 1
  function b() {
    return num
  }
  return b
}

尾调用实际用途——递归函数优化

在ES5时代,我们不推荐使用递归,因为递归会影响性能。
但是有了尾调用优化之后,递归函数的性能有了提升。

//新型尾优化写法
"use strict";  
function a(n, p = 1) {
  if(n <= 1) {
    return 1 * p
  }
  let s = n * p
  return a(n - 1, s)
}
//求 1 x 2 x 3的阶乘
let sum = a(3)
console.log(sum) // 6

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/108417.html

相关文章

  • 深入理解ES6笔记——导读

    摘要:最近买了深入理解的书籍来看,为什么学习这么久还要买这本书呢主要是看到核心团队成员及的创造者为本书做了序,作为一个粉丝,还是挺看好这本书能给我带来一个新的升华,而且本书的作者也非常厉害。 使用ES6开发已经有1年多了,以前看的是阮一峰老师的ES6教程,也看过MDN文档的ES6语法介绍。 最近买了《深入理解ES6》的书籍来看,为什么学习ES6这么久还要买这本书呢?主要是看到Daniel A...

    Godtoy 评论0 收藏0
  • 深入理解JavaScript

    摘要:深入之继承的多种方式和优缺点深入系列第十五篇,讲解各种继承方式和优缺点。对于解释型语言例如来说,通过词法分析语法分析语法树,就可以开始解释执行了。 JavaScript深入之继承的多种方式和优缺点 JavaScript深入系列第十五篇,讲解JavaScript各种继承方式和优缺点。 写在前面 本文讲解JavaScript各种继承方式和优缺点。 但是注意: 这篇文章更像是笔记,哎,再让我...

    myeveryheart 评论0 收藏0
  • 深入理解ES6笔记(十一)Promise与异步编程

    摘要:回调函数模式类似于事件模型,因为异步代码也会在后面的一个时间点才执行如果回调过多,会陷入回调地狱基础可以当做是一个占位符,表示异步操作的执行结果。函数可以返回一个,而不必订阅一个事件或者向函数传递一个回调函数。 主要知识点:Promise生命周期、Promise基本操作、Promise链、响应多个Promise以及集成PromiseshowImg(https://segmentfaul...

    RayKr 评论0 收藏0
  • 深入理解ES6笔记(十)增强的数组功能

    摘要:在可迭代对象上使用所有数组上的新方法与方法与方法均接受两个参数一个回调函数一个可选值用于指定回调函数内部的。回调函数可接收三个参数数组的某个元素该元素对应的索引位置以及该数组自身。 主要知识点:创建数组、数组上的新方法、类型化数组showImg(https://segmentfault.com/img/bVbfWo1?w=991&h=587); 《深入理解ES6》笔记 目录 创建数组...

    pekonchan 评论0 收藏0

发表评论

0条评论

aristark

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<