JS函数进阶学习笔记| 青训营笔记

84 阅读9分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 8 天

1.call、apply、bind

在学习 call()、apply()、bind() 方法之前,我们先来复习一下 this 的指向问题,我们前面说过一个口诀:谁调用 this,它就指向谁。让我们先来看一个例子:

function foods() {}
foods.prototype = {
  price: "¥15",
  say: function () {
    console.log("My price is " + this.price);
  },
};

var apple = new foods();
apple.say(); // My price is ¥15
var orange = new foods();
orange.say(); // My price is ¥15

也就是说上述例子调用 say() 方法,最后打印的结果都是一样的,但是如果我们想打印橘子的价钱是 10 元呢?又不想重新定义 say() 方法。JavaScript 为我们专门提供了一些函数方法用来帮我们更优雅的处理函数内部 this 指向问题。这就是接下来我们要学习的 call()、apply()、bind() 三个函数方法。

call

call() 方法调用一个函数, 其具有一个指定的 this 值和分别地提供的参数(参数的列表)。语法为:

fun.call(thisArg, arg1, arg2, ...)

注:

  • thisArg 指的是在 fun 函数中指定的 this 的值。如果指定了 null 或者 undefined 则内部 this 指向 window,同时值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。是一个可选项。
  • arg1, arg2, ...指定的参数列表。也是可选项。
  • 使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined。
  • call() 允许为不同的对象分配和调用属于一个对象的函数/方法。
  • call() 提供新的 this 值给当前调用的函数/方法。你可以使用 call() 来实现继承:写一个方法,然后让另外一个新的对象来继承它(而不是在新对象中再写一次这个方法)。
  1. 使用 call() 方法调用函数并且指定上下文的 this。前面的例子可以改写成:
function foods() {}
foods.prototype = {
  price: "¥15",
  say: function () {
    console.log("My price is " + this.price);
  },
};

var apple = new foods();
orange = {
  price: "¥10",
};
apple.say.call(orange); // My price is ¥10
  1. 在一个子构造函数中,你可以通过调用父构造函数的 call() 方法来实现继承。在控制台输入如下代码:
function Father(name, age) {
  this.name = name;
  this.age = age;
}

function Son(name, age) {
  Father.call(this, name, age);
  this.hobby = "study";
}

var S1 = new Son("zhangsan", 18);
S1; // Son {name: "zhangsan", age: 18, hobby: "study"}

apply apply() 方法与 call() 方法类似,唯一的区别是 call() 方法接受的是参数,apply() 方法接受的是数组。语法为:

fun.apply(thisArg, [argsArray]);
  1. 使用 apply() 方法将数组添加到另一个数组。 例子:
var array = ["a", "b", "c"];
var nums = [1, 2, 3];
array.push.apply(array, nums);
array; // ["a", "b", "c", 1, 2, 3]

注:concat() 方法连接数组,不会改变原数组,而是创建一个新数组。而使用 push() 是接受可变数量的参数的方式来添加元素。使用 apply() 则可以连接两个数组。

  1. 使用 apply() 方法和内置函数。 例子:
var numbers = [7, 10, 2, 1, 11, 9];
var max = Math.max.apply(null, numbers);
max; // 11

注:直接使用 max() 方法的写法为:Math.max(7, 10, 2, 1, 11, 9);

bind bind() 方法创建一个新的函数(称为绑定函数),在调用时设置 this 关键字为提供的值。并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。语法为:

fun.bind(thisArg[, arg1[, arg2[, ...]]])

注:参数 thisArg:当绑定函数被调用时,该参数会作为原函数运行时的 this 指向。当使用 new 操作符调用绑定函数时,该参数无效。参数:arg1,arg2,...表示当目标函数被调用时,预先添加到绑定函数的参数列表中的参数。

我们创建一个简单的绑定函数例子:

var bin = function () {
  console.log(this.x);
};
var foo = {
  x: 10,
};

bin(); // undefined
var func = bin.bind(foo); // 创建一个新函数把 'this' 绑定到 foo 对象
func(); // 10

我们再来看一个例子:

this.num = 6;
var test = {
  num: 66,
  getNum: function () {
    return this.num;
  },
};

test.getNum(); // 返回 66

var newTest = test.getNum;
newTest(); // 返回 6, 在这种情况下,"this"指向全局作用域

// 创建一个新函数,将"this"绑定到 test 对象
var bindgetNum = newTest.bind(test);
bindgetNum(); // 返回 66
var newTest = test.getNum;
newTest();

// 上面这两行代码其实相当于:
var newTest(){
    return this.num;
}
// 所以 this 指向的是全局作用域,返回 6。

2.递归

在程序中,递归就是函数自己直接或者间接的调用自己。

例子:计算 1 到 10 之间的整数相加的和:

function foo(n) {
  if (n == 0) {
    return 0;
  } // 临界条件
  else {
    return n + foo(n - 1);
  }
}
var a = foo(10);
a; // 55

注:一定要写临界条件,不然程序无法结束并且会报错。

3.作用域

作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。简单来说,作用域的值就是作用范围,也就是说一个变量或函数在什么地方可以使用,在什么地方不能使用。

块级作用域

在 JavaScript 中是没有块级作用域的。比如:

{
  var num = 123;
  {
    console.log(num);
  }
}
console.log(num);

上面的例子并不会报错,而是打印两次 123,但是在其他编程语言中(C#、C、JAVA)会报错,这是因为在 JavaScript 中是没有块级作用域。也就是说,使用 {} 标记出来的代码块中声明的变量 num,是可以被 {} 外面访问到的。

函数作用域

JavaScript 的函数作用域是指在函数内声明的所有变量在函数体内始终是可见的,不涉及赋值。来看个例子:

function test() {
  var num = 123;
  console.log(num);
  if (2 == 3) {
    var k = 5;
    for (var i = 0; i < 10; i++) {}
    console.log(i);
  }
  console.log(k); // 不会报错,而是显示 undefined
}
test();

全局作用域

全局作用域也就是说什么地方都能够访问到。比如我们不用 var 关键字,直接声明变量的话,那这个变量就是全局变量,它的作用域就是全局作用域。使用 window 全局对象来声明,全局对象的属性也是全局变量。另外在所有的函数外部用 var 声明的变量也是全局变量,这是因为内层作用域可以访问外层作用域。

注:

  • 内层作用域可以访问外层作用域,反之不行。
  • 整个代码结构中只有函数可以限定作用域。
  • 如果当前作用规则中有名字了,就不考虑外面的同名变量。
  • 作用域规则首先使用提升规则分析。

变量名提升

JavaScript 是解释型的语言,但是它并不是真的在运行的时候完完全全的逐句的往下解析执行。 例子:

func();

function func() {
  console.log("Hello Xnm");
}

这说明了它并不是完全的逐句往下解析的,否则是会报错的。显然,在执行 func() 之前,引擎就已经解析到了 function func(){},发生了变量名提升。那么变量名提升是在什么时候发生的呢?JavaScript 引擎在对 JavaScript 代码进行解释执行之前,会对 JavaScript 代码进行预解析,在预解析阶段,会将以关键字 var 和 function 开头的语句块提前进行处理。当变量和函数的声明处在作用域比较靠后的位置的时候,变量和函数的声明会被提升到作用域的开头。也就是说上面的代码,我们可以理解为:

function func() {
  console.log("Hello Xnm");
}
func();

再来看看变量声明的例子:

console.log(num);
var num = 10;

这里说的提示,是声明的提升,也就是说上面的代码,我们可以理解为:

var num; // 这里是声明
console.log(num); // 变量声明之后并未有初始化和赋值操作,所以这里是 undefined
num = 10; // 最终打印结果为 10

下面再来看几个复杂一点的例子。

函数同名的时候:

func();
function func() {
  console.log("Hello xnm");
}

func();
function func() {
  console.log("hi xnm");
} // 最终结果打印了两次 hi xnm

上面代码相当于:

function func() {
  console.log("Hello xnm");
}
function func() {
  console.log("hi xnm");
}
func();
func();

函数变量同名的时候:

console.log(foo);
function foo() {}
var foo = 6;

当出现变量声明和函数同名的时候,只会对函数声明进行提升,变量会被忽略。所以上面的代码相当于:

function foo() {}
console.log(foo);
foo = 6;

再来看一种:

var num = 1;
function num() {
  alert(num);
}
num();

上面的代码相当于:

function num() {
  alert(num);
}

num = 1;
num();

4.闭包

闭包是指函数可以使用函数之外定义的变量。

简单的闭包

在 JavaScript 中,使用全局变量是一个简单的闭包实例。比如:

var num = 3;
function foo() {
  console.log(num);
}
foo(); //打印 3

复杂的闭包

function f1() {
  var num1 = 6;
  function f2() {
    var num2 = 7;
  }
  console.log(num1 + num2);
}
f1();

在上述代码中函数 f2 能够访问到它外层的变量 num1,但是 f1 是不能访问 f2 中的变量num2,因此我们可以把 num2 作为 f2 的返回值,然后通过 f2 的返回值就可以访问到 sum2 了。

function f1() {
  var num1 = 6;
  function f2() {
    var num2 = 7;
    return num2;
  }
  console.log(num1 + f2());
}
f1();

6. arguments 对象

在函数代码中,使用特殊对象 arguments,无需明确指出参数名,我们就能访问它们。第一个参数是 arguments[0],第二个参数是 arguments[1],以此类推。比如:

function foo() {
  console.log(arguments[0]);
  console.log(arguments[1]);
}
foo(2, 3); // 打印 2 3

还可以用 arguments 对象检测函数的参数个数,引用属性 arguments.length 即可。来看一个遍历参数求和的例子:

function add() {
  var sum = 0;
  for (var i = 0; i < arguments.length; i++) {
    sum += arguments[i];
  }
  return sum;
}
add(); // 0
add(1); // 1
add(1, 2); // 3
add(1, 2, 3); // 6

7.Function 对象

用 Function() 对象创建函数的语法如下:

var function_name = new Function(arg1, arg2, ..., argN, function_body)

注:每个参数都必须是字符串,function_body 是函数主体,也就是要执行的代码。

例子:

var add = new Function("a", "b", "console.log(a+b);");
add(2, 5); // 打印 7

再看一个例子:

var add = new Function("a", "b", "console.log(a+b);");
var doAdd = add;
doAdd(2, 5); // 打印 7
add(2, 5); // 打印 7

在上述例子中,变量 add 被定义为函数,然后 doAdd 被声明为指向同一个函数的指针。用这两个变量都可以执行该函数的代码,并输出相同的结果。因此,函数名只是指向函数的变量,那么我们可以把函数作为参数传递给另一个函数,比如下面的例子

function addF(foo, b, c) {
  foo(b, c);
}
var add = new Function("a", "b", "console.log(a+b);");
addF(add, 2, 5); // 打印 7

Function 对象的 length 属性

函数属于引用类型,所以它们也有属性和方法。length 属性声明了函数期望的参数个数。

例子:

var add = new Function("a", "b", "console.log(a+b);");
console.log(add.length); // 打印 2

Function 对象的方法

Function() 对象也有与所有对象共享的 valueOf() 方法和 toString() 方法。这两个方法返回的都是函数的源代码。

例子:

var add = new Function("a", "b", "console.log(a+b);");
add.valueOf();
add.toString();