JavaScript Day05 函数详解 2

简介: JavaScript Day05 函数详解

5.作用域

ES5中

函数作用域: 在 JavaScript函数中声明的变量,会成为函数的局部变量。

函数内部声明的变量,在函数外部不能访问。

全局作用域:函数之外声明的变量,会成为全局变量。

函数外部声明的变量,在函数内部可以访问。

当函数嵌套,在这个时候,内部函数与外部函数的这个变量就组成了闭包。

// ES5中没有块级作用域
{
  var a = 10
}
console.log(a); // 10
//全局作用域:global/window/本文件内
var v1 = 10;
v2 = 20; // 所有末定义直接赋值的变量自动声明为拥有全局作用域
function foo() {
  //函数作用域、局部作用域
  var a = 3;
  console.log(v1, v2); 
  console.log(this);
}
foo()
console.log(a); // a is not defined

5.1 案例

var a = 10;
function foo() {
  // 当函数内部有变量a时,会产生局部作用域,外界全局作用域中的a不会对函数内部造成影响
  // 如果把函数内部的变量a注释掉是,函数内部的a输出的就是全局作用域中的a
  console.log(a); //undefined
  var a = 100;
  console.log(a); // 100
  function fn() {
    console.log(a); //undefined
    var a = 200;
    console.log(a); // 200
  }
  fn()
}
foo()

作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突

5.2 作用域链

5.2.1 什么是自由变量

首先认识一下什么叫做 自由变量 。如下代码中,console.log(a)要得到a变量,但是在当前的作用域中没有定义a(可对比一下b)。当前作用域没有定义的变量,这成为 自由变量 。自由变量的值如何得到 —— 要到创建这个函数的那个父级作用域寻找,如果没有就一直向上级祖先元素寻找(这就是所谓的"静态作用域")

var a = 100
function fn() {
    var b = 200
    console.log(a) // 这里的a在这里就是一个自由变量  // 100
    console.log(b)
}
fn()

5.3.2 什么是作用域链

如果父级也没呢?再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链 。

var a = 100
function F1() {
  var b = 200
  function F2() {
    var c = 300
    console.log(a) // 自由变量,顺作用域链向父作用域找 //100
    console.log(b) // 自由变量,顺作用域链向父作用域找 //200
    console.log(c) // 本作用域的变量  //300
  }
  F2()
}
F1()

5.3.3 综合案例-作用域链-闭包-经典面试题

var a = 10
function fn() {
  var b = 20
  function bar() {
    console.log(a + b) //30
  }
  return bar
}
var x = fn(), // 执行fn() 返回的是bar
b = 200
x() //执行x,就是执行bar函数

aef5d84bf2f44f7ba3092d2fb394f70f.png

6.函数调用

函数声明好之后并不会直接运行,需要进行调用才能运行。

6.1 调用函数的方式不仅限于()执行,还有其他几种方式

  • 函数名(实参列表);
  • 函数名.call(执行环境对象,实参列表);
  • 函数名.apply(执行环境对象,实参列表数组);
  • 函数名.bind(执行环境对象)(实参列表);

先看一个案例

var obj = {
  name: 'briup',
  sayName: function () {
    console.log(this.name);
  }
}
var b = obj.sayName;
b(); //undefined
obj.sayName(); // briup

那我们有什么方法可以让b(),this指向到obj中呢?

有时候我们不得不将这个对象保存到另外的一个变量中,那么就可以通过以下方法。

6.2 改变this指向的方法

6.2.1 call(执行环境对象,实参列表);

调用call方法,第一个参数就是要把b添加到哪个环境中,简单来说,this就会指向那个对象。

var obj = {
  name: 'briup',
  sayName: function () {
    console.log(this.name);
  }
}
var b = obj.sayName;
b.call(obj); // briup

在使用call调用的时候,还可以传递多个参数

var obj = {
  name: 'briup',
  sayName: function (a,b) {
    console.log(this.name);
    console.log(a,b); // 1,2
  }
}
var b = obj.sayName;
b.call(obj,1,2); // briup

6.2.2 apply(执行环境对象,实参列表数组);

apply方法和call方法有些相似,它也可以改变this的指向

var obj = {
  name: 'briup',
  sayName: function () {
    console.log(this.name);
  }
}
var b = obj.sayName;
b.apply(obj); // briup

同样apply也可以有多个参数,但是不同的是,第二个参数必须是一个数组,如下:

var obj = {
  name: 'briup',
  sayName: function (a,b) {
    console.log(this.name);
    console.log(a,b); // 100,200
  }
}
var b = obj.sayName;
b.apply(obj,[100,200]); // briup

注意:如果call和apply的第一个参数是null,那么this在node环境下指向的是global对象,在HTML中指向的是window对象

var obj = {
  name: 'briup',
  sayName: function () {
    console.log(this); // global
  }
}
var b = obj.sayName;
b.apply(null); // briup

6.2.3 bind(执行环境对象)(实参列表);

var obj = {
  name: 'briup',
  sayName: function () {
    console.log(this.name);
  }
}
var b = obj.sayName;
b.bind(obj); // 代码没有被打印,这就是bind和call、apply方法的不同,实际上bind方法返回的是一个修改过后的函数。
// 新建一个变量c来接收bind修改后的函数
var c = b.bind(obj);
console.log(c); // 发现c是一个[Function: bound sayName]函数
// 执行c
c(); // briup

同样bind也可以有多个参数,并且参数可以执行的时候再次添加,但是要注意的是,参数是按照形参的顺序进行的。

var obj = {
  name: 'briup',
  sayName: function (a,b,c) {
    console.log(this.name);
    console.log(a,b,c); // 1,2,3
  }
}
var b = obj.sayName;
b.bind(obj); // 代码没有被打印,这就是bind和call、apply方法的不同,实际上bind方法返回的是一个修改过后的函数。
// 新建一个变量c来接收bind修改后的函数
var c = b.bind(obj, 1, 2);
console.log(c); // 发现c是一个[Function: bound sayName]函数
// 执行c
c(3); // briup

总结:call和apply都是改变上下文中的this并立即执行这个函+++++数,bind方法可以让对应的函数想什么时候调就什么时候调用,并且可以将参数在执行的时候添加,这是它们的区别,根据自己的实际情况来选择使用。

7.函数的应用

函数本质上是一种对象,可以将其当做普通对象来使用。

回调函数

7.1 回调

就是回头调用的意思。主函数的事先做完,回头再调用传进来的那个函数。

//定义主函数,回调函数作为参数
function A(callback) {
  callback();
  console.log('我是主函数');
}

7.2 案例

//定义主函数,回调函数作为参数
function A(callback) {
  callback();
  console.log('我是主函数');
}
//定义回调函数
function B() {
  // 模仿延时操作
  setTimeout(() => {
    console.log('我是回调函数');
  }, 3000);
}
//调用主函数,将函数B传进去
A(B);

7.3 回调函数的作用:

回调函数一般都用在耗时操作上面:因为主函数不用等待回调函数执行完,可以接着执行自己的代码。比如ajax请求,比如处理文件等。

作为返回值

我们在作用域链的案例中,就使用到了函数作为返回值的用法

var a = 10
function fn() {
  var b = 20
  function bar() {
    console.log(a + b) //30
  }
  return bar
}
var x = fn(), // 执行fn() 返回的是bar
b = 200
x() //执行x,就是执行bar函数

8.闭包

8.1 什么是闭包?

简单讲,闭包就是指有权访问另一个函数作用域中的变量的函数。

MDN 上面这么说:闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。

8.2 闭包的生成有三个必要条件

  • 函数嵌套函数
  • 内部函数引用了外部函数中的数据(属性、函数)
  • 参数和变量不会被回收

这样就形成了一个不会销毁的函数空间

8.2.1 产生一个闭包

**创建闭包最常见方式,就是在一个函数内部创建另一个函数。**下面例子中的 closure 就是一个闭包:

function func() {
  var a = 1, b = 2;
  function closure() {
    return a + b;
  }
  return closure;
}
console.log(func()()); // 3

闭包的作用域链包含着它自己的作用域,以及包含它的函数的作用域和全局作用域。

在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

8.2.2 闭包的用途

闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

function f1() {
  var n = 999;
  nAdd = function () { n += 1 }
  function f2() {
    console.log(n);
  }
  return f2;
}
var result = f1();
result(); // 999
nAdd();
result(); // 1000

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。


为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。


这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

8.2.3 使用闭包的注意点

1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露,这是IE的BUG。解决方法是,在退出函数之前,将不使用的局部变量全部删除。


2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。多个子函数的scope都是同时指向父级,是完全共享的。因此当父级的变量对象被修改时,所有子函数都受到影响。

8.2.4 闭包中的this指向问题-初体验

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    var name = 'window'
    var obj = {
      name: 'obj',
      say: function () {
        return function () {
          console.log(this.name);
        }
      }
    }
    var x = obj.say()
    x()
  </script>
</body>
</html>


目录
相关文章
|
5天前
|
JavaScript 前端开发
JavaScript 闭包:让你更深入了解函数和作用域
JavaScript 闭包:让你更深入了解函数和作用域
|
1天前
|
前端开发 JavaScript
前端 JS 经典:函数管道
前端 JS 经典:函数管道
4 0
|
5天前
|
JavaScript 前端开发 网络架构
函数柯里化:JavaScript中的高级技巧
函数柯里化:JavaScript中的高级技巧
|
5天前
|
JavaScript 前端开发
JavaScript的`apply`方法:函数的“应用”与“调用”
JavaScript的`apply`方法:函数的“应用”与“调用”
|
5天前
|
JavaScript 前端开发
JavaScript的`bind`方法:函数的“复制”与“定制”
JavaScript的`bind`方法:函数的“复制”与“定制”
|
5天前
|
JavaScript 前端开发
JavaScript的`call`方法:实现函数间的调用!
JavaScript的`call`方法:实现函数间的调用!
|
6天前
|
JavaScript 前端开发
在JavaScript中,函数原型(Function Prototype)是一个特殊的对象
【5月更文挑战第11天】JavaScript中的函数原型是一个特殊对象,它为所有函数实例提供共享的方法和属性。每个函数在创建时都有一个`prototype`属性,指向原型对象。利用原型,我们可以向所有实例添加方法和属性,实现继承。例如,我们定义一个`Person`函数,向其原型添加`greet`方法,然后创建实例`john`和`jane`,它们都能调用这个方法。尽管可以直接在原型上添加方法,但推荐在构造函数内部定义以封装数据和逻辑。
18 2
|
6天前
|
前端开发 JavaScript 数据处理
在JavaScript中,异步函数是指什么
【5月更文挑战第9天】JavaScript中的异步函数用于处理非立即完成的操作,如定时器、网络请求等。它们可通过回调函数、Promise或async/await来实现。示例展示了如何使用async/await模拟网络请求:定义异步函数fetchData返回Promise,在另一异步函数processData中使用await等待结果并处理。当fetchData的Promise解析时,data变量接收结果并继续执行后续代码。注意,调用异步函数不会阻塞执行,而是会在适当时间点继续。
12 0
|
6天前
|
自然语言处理 JavaScript 前端开发
在JavaScript中,this关键字的行为可能会因函数的调用方式而异
【5月更文挑战第9天】JavaScript中的`this`关键字行为取决于函数调用方式。在非严格模式下,直接调用函数时`this`指全局对象,严格模式下为`undefined`。作为对象方法调用时,`this`指向该对象。用`new`调用构造函数时,`this`指向新实例。通过`call`、`apply`、`bind`可手动设置`this`值。在回调和事件处理中,`this`可能不直观,箭头函数和绑定方法可帮助管理`this`的行为。
15 1
|
6天前
|
JavaScript 前端开发 网络架构
JavaScript中的箭头函数是一种新的函数表达形式
【5月更文挑战第9天】JavaScript的箭头函数以简洁语法简化函数定义,其特性包括:1) 不绑定自身this,继承上下文的this,适合回调和事件处理;2) 没有arguments对象,需用剩余参数语法访问参数;3) 不能用作构造函数,无new调用;4) 没有prototype属性,不支持基于原型的继承。箭头函数在特定场景下优化了this处理,但使用时要注意与普通函数的差异。
12 2