JavaScript常见的考点以及容易混淆的知识点---Day05

文章详细介绍了JavaScript中的函数,包括函数声明、内部属性如arguments和this,IIFE的概念和用途,作用域链以及自由变量,还探讨了函数调用的不同方式,最后讲解了闭包的定义、用途和注意事项,以及闭包中的this指向问题。
摘要由CSDN通过智能技术生成

目录

一、函数

1.函数介绍:

2.函数声明 :

3.函数内部属性:

(1)arguments

(2)callee属性

(3)this:

4.IIFE:

(1)IIFE的写法: 

(2)IIFE的基本使用:

5.作用域:

(1)作用域链 :

什么是自由变量

什么是作用域链

6.函数调用:

7.函数的应用

(1)回调函数:

(2)作为返回值:

8.闭包 

(1)什么是闭包?

(2) 闭包的用途:

(3) 使用闭包的注意点:

(4)闭包中的this指向问题: 


一、函数

1.函数介绍:

        (1)函数允许我们封装一系列代码来完成特定任务。当想要完成某一任务时,只需要调用相应的代码即可。方法(method)一般为定义在对象中的函数。

        JavaScript 使用关键字 function 定义函数。

        函数可以通过声明定义,也可以是一个表达式

        (2)函数的作用:

  • 功能的封装,直接调用,代码复用率提高
  • 构建对象的模板(构造函数)
  • 函数实际上是对象,每个函数都是Function类型的实例,并且都与其他引用类型一样具有属性和方法,由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。

2.函数声明 :

        自定义函数: 函数由function关键字声明,后面紧跟函数名,函数名后面为形参列表,列表后大括号括起来的内容为函数体。也可以将一个匿名函数(没有函数名的函数)赋值给一个函数变量,这种方式称为函数表达式。 解析器在向执行环境中加载数据时,会率先读取函数声明,并使其在执行任何代码之前可用;当执行器执行到函数表达式的代码的时候才会真正的解释执行。 表示方法:

(1)函数声明:

function 函数名(形参列表){
    //函数体
}

(2)函数表达式:

var 函数名 = function(形参列表){
    //函数体
}

        这种形式看起来像常规的变量赋值,先创建一个匿名函数,然后赋值给变量函数名。在JS中如果要想使用一个变量,必须先给这个变量赋值,所以函数表达式也不例外,在调用之前,必须先给它赋值。否则会报错。

(3)函数声明与var变量声明类似,会进行提升:

function add(a,b){
  var result = a + b;
  return result;//返回值//返回执行的结果给被调用的
}
var total = add(1,2)

foo();//函数声明提升到代码的最前边,可以直接调用函数
function foo(){
  console.log("hell world");
  //return;
  //console.log("1");//return之后的语句不执行
  //如果没有返回的内容,则在写代码的时候不关注返回值
  //没有return:代码执行到大括号
}
//变量声明提升  变量声明提升到代码的前边,函数声明之后正常代码之前
console.log(a);   //undefined  这里不报错,因为后边有var a的声明。变量的声明进行提升到前边
var a = 'hello';  
console.log(a);   //'hello'

3.函数内部属性:

        只有在函数内部才能访问的属性。this也可以在函数外部进行使用。

(1)arguments

        ECMAScript函数的参数与大多数其他语言中的函数的参数有所不同,ECMAScript函数不介意传递参数的个数以及参数类型这是因为函数的参数在函数内部是使用一个类数组对象来表示的。这个类数组对象就是arguments。

        arguments是一个类数组对象,包含着传入函数中的所有参数。arguments主要用途是保存函数参数,但是这个对象还有一个名为callee的属性,该属性是一个指针,指向拥有这个arguments对象的函数。

//length声明时希望的参数的个数
function add(a,b){
  var result = a + b;
  return result;
}
console.log(add.length);//表示函数希望接受的命名参数的个数,即形参的个数。
Object.getOwnPropertyDescriptor(add,'length');
add(22,42,12,44);
/*参数:接收实参的快捷方式
	函数外部:实参保存的就是原始数据
	函数内部:arguments保存真正的实参
*/
/*arguments类数组对象
	arguments={
	“0”:1,
	“1”:33,
	“2”:”zs”
	};
	arguments[0],arguments[“1”]
*/
function add(a,b){
  console.log(arguments[0],arguments[1],arguments[2],arguments[3]);
  console.log(a+b);
}
add(10);
//10 undefined undefined undefined
//NaN
add(10,20);
//10 20 undefined undefined
//30
add(10,20,30);
//10 20 30 undefined
//30
add(10,20,30,40);
//10 20 30 40
//30

(2)callee属性

        callee属性是arguments对象的一个成员,仅当相关函数正在执行时才可用

        callee属性的初始值是正被执行的Function对象

// 实现匿名的递归函数
function factorial(num){
    if(num<=1){
        return 1;
    }else {
        return num * arguments.callee(num-1);
    }
}
console.log(factorial(4)); //24

        但是如果代码是在严格模式下开发,使用"use strict";则会出现报错信息:

TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them

 

         在严格模式下不能通过脚本访问arguments.callee,访问这个属性会报错,那么可以使用命名函数表达式来达到相同的结果:


"use strict";
var factorial = (function f(num){
     if(num<=1){
        return 1;
    }else {
        return num * f(num-1);
    }
})

console.log(factorial(4)); //24

(3)this:

        面向对象语言中 this 表示当前对象的一个引用。

        但在 JavaScript 中 this 不是固定不变的,它会随着执行环境的改变而改变。

  • 在方法中,this 表示该方法所属的对象。
  • 如果单独使用,this 表示全局对象。
  • 在函数中,this 表示全局对象。
  • 在事件中,this 表示接收事件的元素。
  • 在显式函数绑定时,我们可以自己决定this的指向

实例: 

var person = {
  firstName: "LeBron",
  lastName : "James",
  id       : 8888,
  fullName : function() {
    return this.firstName + " " + this.lastName;
  }
};

(1)方法中的 this

        在对象方法中, this 指向调用它所在方法的对象。

        在上面一个实例中,this 表示 person 对象。

        fullName 方法所属的对象就是 person。

fullName : function() {  return this.firstName + " " + this.lastName; }

(2)单独使用this:

        单独使用 this,则它指向全局对象。

        在浏览器中,window 就是该全局对象为 [object Window]:

        在node中,指向的是一个{}

var x = this;

(3)函数中使用this(默认):

        在函数中,函数的所属者默认绑定到 this 上。

        在浏览器中,window 就是该全局对象为 [object Window]:

        在node中,指向的就是global对象

function myFunction() {  return this; }

(4)事件中的this:

        在 HTML 事件句柄中,this 指向了接收事件的 HTML 元素:

<button onclick="this.style.display='none'"> 点我后我就消失了 </button>

(5)显示函数绑定:

        在 JavaScript 中函数也是对象,对象则有方法,apply 和 call 就是函数对象的方法。这两个方法异常强大,他们允许切换函数执行的上下文环境(context),即 this 绑定的对象。

        在下面实例中,当我们使用 person2 作为参数来调用 person1.fullName 方法时, this 将指向 person2, 即便它是 person1 的方法:

var person1 = {
  fullName: function () {
    return this.firstName + " " + this.lastName;
  }
}
var person2 = {
  firstName: "Zach",
  lastName: "Muyi",
}
var name = person1.fullName.call(person2);  // 返回 "Zach Muyi"
console.log(name);

4.IIFE:

        IIFE: Immediately Invoked Function Expression,意为立即调用的函数表达式,也就是说,声明函数的同时立即调用这个函数。

作用:

  • 页面加载完成后只执行一次的设置函数。
  • 将设置函数中的变量包裹在局部作用域中,不会泄露成全局变量。

对比,不采用IIFE时的函数声明和函数调用 :

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

下面是IIFE形式的函数调用:

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

        函数的声明和IIFE的区别在于,在函数的声明中,我们首先看到的是function关键字,而IIFE我们首先看到的是左边的(。也就是说,使用一对()将函数的声明括起来,使得JS编译器不再认为这是一个函数声明,而是一个IIFE,即需要立刻执行声明的函数。两者达到的目的是相同的,都是声明了一个函数foo并且随后调用函数foo。

(1)IIFE的写法: 

        对返回结果不进行处理:

(function(形参){
	函数体内容
})(实参);

        对返回结果不进行处理:

(function(形参){
	函数体内容
}(实参));

        返回的是一个布尔值,然后进行取反

!function(形参){
	函数体内容
}(实参)

        对于数字返回的是原来的结果,非数字返回NaN

+function(形参){
	函数体内容
}(实参)

        对于数字返回的是正负符号相反,非数字返回NaN

-function(形参){
	函数体内容
}(实参)

        对于数字返回的是正负符号相反再减1,非数字返回-1

~function(形参){
	函数体内容
}(实参)

        返回的结果是undefined

void function(形参){
	函数体内容
}(实参)

(2)IIFE的基本使用:

// 就像其它任何函数一样,一个立即执行函数也能返回值并且可以赋值给其它变量。
var sum = (function (a,b) {
  return a + b;
}(1,2))
console.log(sum);

案例:

for (var i = 0; i < 6; i++) {
  function output() {
    console.log(i); // 为什么输出的是6,而不是0,1,2,3,4,5
    // 因为输出的 i 是全局作用域的,当循环结束后 i 的值是 6,所以输出的 i 就是6。
  }
}
output()
for (var i = 0; i < 6; i++) {
  (function (j) {
    console.log(j); //0,1,2,3,4,5
  })(i)
  // 因为 JS 中调用函数传递参数都是值传递 ,所以当立即执行函数执行时,首先会把参数 i 的值复制一份,然后再创建函数作用域来执行函数,循环5次就会创建5个作用域,所以每个输出访问的都是不同作用域的 i 的值 。
}

为什么需要IIFE?

        如果只是为了立即执行一个函数,显然IIFE所带来的好处有限。实际上,IIFE的出现是为了弥补JS(ES5)在作用域方面的缺陷:JS只有全局作用域(global scope)函数作用域(function scope),从ES6开始才有块级作用域(block scope)。对比现在流行的其他面向对象的语言可以看出,JS在访问控制这方面是多么的脆弱!那么如何实现作用域的隔离呢?在JS中,只有function才能实现作用域隔离,因此如果要将一段代码中的变量、函数等的定义隔离出来,只能将这段代码封装到一个函数中。在我们通常的理解中,将代码封装到函数中的目的是为了复用。在JS中,当然声明函数的目的在大多数情况下也是为了复用,但是JS迫于作用域控制手段的贫乏,我们也经常看到只使用一次的函数:这通常的目的是为了隔离作用域了!既然只使用一次,那么立即执行好了!既然只使用一次,函数的名字也省掉了!这就是IIFE的由来。

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

案例:

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()

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

(1)作用域链 :

  • 什么是自由变量

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

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

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

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()

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

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函数

6.函数调用:

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

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

  • 函数名(实参列表);
  • 函数名.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中呢?

(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

(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

(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.函数的应用

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

(1)回调函数:

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

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

 案例:

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

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

(2)作为返回值:

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

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

(1)什么是闭包?

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

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

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

  • 函数嵌套函数

  • 内部函数引用了外部函数中的数据(属性、函数)

  • 参数和变量不会被回收

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

  • 产生一个闭包

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

function func() {
  var a = 1, b = 2;

  function closure() {
    return a + b;
  }
  return closure;
}
console.log(func()()); // 3

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

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

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

(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,可以在函数外部对函数内部的局部变量进行操作。

(3) 使用闭包的注意点:

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

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

(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";
			console.log(this)   // window对象
      var obj = {
        name: "obj",
        say: function () {
					console.log(this.name) //obj
          return function () {
            console.log(this.name);// window
          };
        },
      };
      var x = obj.say();
      x();
    </script>
  </body>
</html>

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值