Js 进阶 [this,高阶函数闭包,深拷贝,浅拷贝,原型,原型链,继承,事件循环EventLoop,防抖,节流,柯里化]

984 阅读22分钟

前言

本章分享常用Js核心知识点 ,开发必会,面试必备

目录

  • 深拷贝 ,浅拷贝
  • 作用域,作用域链,执行上下文
  • 关键字 this
  • 高阶函数 - 闭包
  • 原型,原型链
  • Js 面向对象 - 封装 - 继承
  • 执行机制 - 事件循环 - Event Loop
  • 函数防抖与节流
  • 函数柯里化

一,深拷贝,浅拷贝

如何理解深拷贝和浅拷贝呢

深拷贝是创建一个新的内存地址保存值 ; (与原对象互不影响) 浅拷贝是对象的属性的引用,而不是对象本身; (浅拷贝只拷贝一层,如果存在多层还是会影响原对象) 说可能不尽然能够理解,我们用代码实践一下

浅拷贝 ,对值的引用 , 结果可以发现修改了 obj2 属性的值, obj 也相应发生了改变 ;

var obj ={a:1,b:2}
var obj2 = obj
obj2.a = 2
// obj2 {a: 2, b: 2}
// obj {a: 2, b: 2}

深拷贝 ,是开辟了一块儿新的内存地址另存一份,两者互不影响,同样,我们用代码实践一下,结果可以发现,我们同样的方式修改了 obj2 的属性,obj 与 obj2 打打印结果完全不一样;

function clone(source) {
    var target = {};
    for(var i in source) {
        if (source.hasOwnProperty(i)) {
            target[i] = source[i];
        }
    }
    return target;
}
var obj = {a:1,b:2}
var obj2 = clone(obj)
obj2.a = 2
// obj2 {a: 2, b: 2}
// obj {a: 1, b: 2}

以上的两个实例只是对单层数据格式的实现,其实多层也是一样的,实现深拷贝的方式有很多种,在这里我列举几种常见多层深拷贝的例子供童鞋们参考,拿对象来实践 ;

JSON.stringify(),通过对对象的格式转换,赋值,再转回来 缺点:包含有new Date 或者函数的对象 no

var obj = {a:1,b:2,c:3}
var obj2 = JSON.stringify(obj)
obj2 = JSON.parse(obj2)
//就完完全全的复制一份obj出来啦 

手写递归的方式

function clone(source) {
    var target = {};
    for(var i in source) {
        if (source.hasOwnProperty(i)) { // hasOwnProperty 判断对象是否包含 i ; 
            if (typeof source[i] === 'object') { 
                target[i] = clone(source[i]); // 注意这里 , 递归操作
            } else {
                target[i] = source[i];
            }
        }
    }

    return target;
}
var obj = {a:1,b:2,c:3}
var obj2 = clone(obj)

二,作用域,作用域链,执行上下文

作用域,作用域链

JavaScript 有全局作用域、函数作用域和块级作用域(ES6新增)。 我们可以理解为作用域就是一个独立的地盘,拥有自己的属性,变量和方法

  1. 全局作用域:不用多说,哪里都可以访问的变量
  2. 函数作用域:就是在这个函数体内才能访问的变量;可以利用闭包来实现跨函数访问局部作用域
  3. 块级作用域:ES6新增,用let命令新增了块级作用域,私有作用域,不会被污染

我们通过一个小例子来描述一下具体得到作用域场景

var a = 100
function F1() {
   var a = 200;
  return function () {
  	console.log(a)
  }
}
function F2(f1) {
  var a = 300
  console.log(f1())
}
var f1 = F1()
F2(f1) // 200
// 解析
// 首先 var f1 = F1() 
// F1() 执行返回一个匿名函数 function () {console.log(a);} 也就是说传进了一个匿名函数
// 然后执行 F2(f1) ,变量 f1 是一个匿名函数
// F2 执行代码体,打印 f1() ,那么 f1() 的作用域到底在哪儿呢 ?  

上述代码中,变量a的值,从函数F1中查找而不是F2,这是因为全局变量从作用域链中去寻找(也就是定义匿名函数的地方依次向上查找),而不是函数执行时去寻找;所以结果是 200 , 如果变成下边这段代码呢 ?

var a = 100
function F1() {
  return function () {
  	console.log(a)
  }
}
function F2(f1) {
  var a = 300
  console.log(f1())
}
var f1 = F1()
F2(f1) // 100

上边说到是从定义时的作用域链去寻找,那么找到 F1 函数作用域,没有找到,此时会向上一层继续查找,所以输出结果是 100

如果上一层也没呢?再一层一层向上寻找,直到找到全局作用域还是没找到,就只能说 game over。这种一层一层的关系,就是 作用域链

执行上下文

说到执行上下午,可能很多小伙伴们都比较懵逼,说实在的,我之前也是一脸懵逼,经过一番醒悟之后,现在分享给大家 执行上下文就是一个抽象的概念,Js的任何代码动作都是在执行上下文中进行的

执行上下文: 由 Js引擎自动创建的对象, 包含对应作用域中的所有变量属性 执行栈: 用来管理产生的多个执行上下文

执行上下文创建和初始化过程

全局

  • 在执行全局代码前将window确定为全局执行上下文
  • 对全局数据进行预处理
    • var定义的全局变量==>undefined, 添加为window的属性
    • function声明的全局函数==>赋值(fun), 添加为window的方法
    • this==>赋值(window),确定 this
  • 开始执行全局代码

函数

  • 在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象(虚拟的, 存在于栈中)
  • 对局部数据进行预处理
    • 形参变量==>赋值(实参)==>添加为执行上下文的属性
    • arguments==>赋值(实参列表), 添加为执行上下文的属性
    • var定义的局部变量==>undefined, 添加为执行上下文的属性 , 声明提前
    • function声明的函数 ==>赋值(fun), 添加为执行上下文的方法 ,声明提前
    • this==>赋值(调用函数的对象), 确定 this
  • 开始执行函数体代码

那什么是执行栈,我们通过一张网络图(慕课手记)来了解一下 在这里插入图片描述 一目了然,可以看的出来,执行栈是先进后出,全部函数环境执行完了,全局环境才会出栈,也就意味着结束,我们用代码来实施一下这个过程

// 引用 慕课手记 的图示来示例一下:
console.log(1);
function pFn() {
    console.log(2);
    (function cFn() {
        console.log(3);
    }());
    console.log(4);
}
pFn();
console.log(5);
// 输出:1 2 3 4 5

解析: 首先进入 window.onload 全局环境 页面加载事件 打印 1 ,全局环境压入栈底 然后执行函数 pFn 进入执行栈 打印 2 然后执行闭包函数 cFn 进入执行栈 打印 3 , cFn 函数体执行完, 出栈 紧跟着 打印 4 , pFn 函数体执行完, 出栈 最后打印 5 ,全局环境出栈

结论: JavaScript单线程,所有的代码都是自上而下执行。 浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的低部。 每进入一个函数,就会为这个函数创建一个执行上下文,并且把它压入栈顶,执行完,就出栈,等待垃圾回收 最后全局执行上下文出栈,有且只有一个

关键字 this

this 是啥 ?

this是 JavaScript 语言的一个关键字,它代表函数运行时,自动生成的一个内部对象;有人说this是水性杨花的,也有人说this很难确定指向,在这里我向告诉大家:随着函数使用场景的不同,this的值会发生变化。但是有一个总的原则,那就是this总是指向调用函数的那个对象。(Js 一切皆对象 emmm ~~~)

先搞明白一个很重要的概念 —— this的值是在执行的时候才能确认,定义的时候不能确认! 为什么呢 —— 因为this是执行上下文环境的一部分,而执行上下文需要在代码执行之前确定,而不是定义的时候;还有一点就是 this 不能赋值;下面通过几个例子来阐述一下

在构造函数中

var x = 1;
function test() {
   console.log(this.x);
}
test();  // 1
// 结论:this 指向全局 window 

在普通对象中

var x = 0 ;
function test() {
  console.log(this.x);
}
var obj = {};
obj.x = 1;
obj.m = test;

obj.m(); // 1
// 结论: this 指向 obj 

New 一个新的构造函数

var x = 1;
function test() {
 this.x = 2;
}
var obj = new test();
obj.x // 2
x //1
// 结论 this 指向 new 出来的构造函数的实例对象, 也就是 obj

闭包中的this

var x = 1;
var object = {
    x: 2,
    getX: function() {
        return function() {
            return this.x;
        };
    }
}
conosole.log( object.getX()() ); // 1
// 分析:object.getX() , 是调用对象 object 里的函数 getX() 
// 此时 this 按照上边所说应该是指向对象 object 
// 我们把 object.getX()(); 拆开来写就是
// var fun = object.getX();  return 一个匿名函数 赋值给变量 fun
// 匿名函数 fun() 调用, 此时的执行环境是全局, 所有指向 window 
// 所以执行结果是 1 

那列举了这么些例子,我们可以看出,this 的指向是有一定规律的,也验证我上边说的那句话:

**this总是指向调用函数的那个对象**

最后总结一下

  • this是Js语言的一个关键字
  • this在函数执行的时候才能确定,因为 this 是执行上下文的一部分,执行上下文需要在函数体代码执行之前确定,而不是定义的时候
  • this总是指向调用函数的那个对象
  • 注意:严格模式略有不同

其实 this 也是可以改变的,通过 bind,call,apply , 具体使用方式自行度娘

高阶函数 - 闭包

闭包是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。

啥是闭包?

百度百科上说:闭包就是能够读取其他函数内部变量的函数。在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

简单点说:就是定义在一个函数内部的函数,就形成了一个闭包;通过一个例子来看看 注释:内部函数需要引用外部函数的变量并且触发,才能确切的说形成了闭包

 function f1(){
    var n=999;
    return function (){
      alert(n); 
    }
  }
  var result=f1();
  result(); // 999 
  // 函数 f1 内部定义了一个匿名函数 ,形成了闭包
  // 假设需要在外部作用域中使用 函数 f1 内部变量 n 
  // 那么就需要通过 f1 函数返回一个内部函数 , 这个内部函数返回 f1 函数局部变量 n
  // 这样我们就是通过以上方式去访问 函数内部的变量

不知道这样诠释,童鞋们是否可以理解,其实闭包使用的场景或者功能性的还是挺多的;

闭包的用途

1,在外部读取函数内部的变量 2,这些变量始终保存在内存中(变相的储存,不被污染),延长局部变量的生命周期 ;

闭包优缺点:

优点:可以访问函数内部的变量,并且不受污染,始终保存在内存中 缺点:就是闭包会使函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成页面的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部清除。 变量 = null

内存溢出与内存泄露

内存溢出 一种程序运行出现的错误,当程序运行需要的内存超过了剩余的内存时, 就出抛出内存溢出的错误

常见内存泄露 1,意外的全局变量 2,没有及时清理的计时器或回调函数 3,闭包

原型,原型链

原型:在JavaScript中原型是一个prototype对象,用于表示类型之间的关系。 原型链:JavaScript 一切皆对象,对象与对象存在着继承关系,通过prototype原型指向父对象,一直到Object,这样就形成了一条链条,专业术语称原型链

我们来详细介绍一下

prototype 显式原型

每一个函数创建的时候就为其自动创建一个 prototype 属性,子类会继承这个这属性,比如:

// 虽然写在注释里,但是你要注意:prototype 是函数才会有的属性
function Person() {} // 构造函数 Person
Person.prototype.name = 'Hisen'; //构造函数原型挂载一个属性 name 赋值为 Hisen
var person = new Person(); // 基于构造函数创建一个实例对象 person 
console.log(person.name) // Hisen

让我们用一张图来表示构造函数与 prototype 之间的关系

那么我们该怎么表示实例与构造函数原型,也就是person和Person.prototype之间的关系呢 ?

__proto__隐式原型

每一个JavaScript对象都具有的一个属性,叫__proto__,这个属性会指向该实例对象构造函数的显示原型

function Person() {}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true 可以看出 显示原型 === 隐式原型  

//其实,实例对象与构造函数关也是有关系的 -- constructor 后边有讲
console.log(prson.constructor === Person) // true

于是我们更新下关系图: 在这里插入图片描述 既然实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数呢 ?答案当然是有的;

constructor

每个原型都有一个constructor属性指向关联的构造函数

function Person() {}
console.log(Person === Person.prototype.constructor); //true

所以再更新关系图: 在这里插入图片描述 通过以上几个例子,我们明白了原型,构造函数,实例对象之间的相互关系;那原型链在哪里 ? 别着急,我们接着往下看:

原型链

当读取实例对象的属性时,如果找不到,就会查找与实例对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层对象 Object 为止 , 还是找不到宣布放弃,返回 null .

我们通过一个例子来看一下

function Person() {}
Person.prototype.name = 'Hisen';
var person = new Person(); // new 一个子类
person.name = 'Hisen child'; // 给子类添加属性 name 并赋值
console.log(person.name)  // Hisen child 
delete person.name; //删除  person.name ; 
console.log(person.name) // Hisen 则会找原型中的name,也就是父类 Person 原型的 name

可以看到删除 person.name 之后,会往上一层查找,也就是 person.__proto__ 注解一下:( person._proto_== Person.prototype ) 也就是从父类的原型中去查找 ( Person.prototype ) , 此时找到了 Hisen , 打印 Hisen 那如果父类的原型中 ( Person.prototype )也没有呢 ? 原型的原型又是什么呢 ?

在前面,我们已经讲了原型也是一个对象,既然是对象,我们就可以用最原始的方式创建它,那就是:

var obj = new Object();
obj.name = 'Hisen'
console.log(obj.name) // Hisen 

所以原型对象是通过Object构造函数生成的,结合之前所讲,实例的__proto__指向构造函数的prototype,所以我们再更新下关系图: 在这里插入图片描述

那Object.prototype的原型呢 ? null,就是null,所以查到Object.prototype就可以停止查找了 所以最后一张关系图就是 在这里插入图片描述 最后说一下,图中蓝色的这条线就是原型链

原型和原型链关系图来源,作者:浪里行舟 https://github.com/ljianshu/Blog/issues/18

Js 面向对象 - 封装 - 继承

封装

面向对象有三大特性,封装、继承和多态。对于ES5来说,没有class(类)的概念,并且由于JS的函数级作用域(函数内部的变量在函数外访问不到),所以我们就可以模拟 class (类)的概念,在ES5中,类其实就是保存了一个函数的变量,这个函数有自己的属性和方法;将属性和方法组成一个类的过程就是封装,在这里我们不做多的分析。主要来说说继承

继承

在 Js 中可以说,继承是有很多方式的,当然每一种都有利弊,下边我们列举几种常见的分析一下:

原型继承

// 创建父构造函数
function SuperClass(name){
  this.name = name;
  this.showName = function(){
    alert(this.name);
  }
}
// 设置父构造器的原型对象属性  age
SuperClass.prototype.age= '18';
// 创建子构造函数
function SubClass(){}
// 设置子构造函数的原型等于父构造函数的原型
SubClass.prototype = SuperClass.prototype; // 原型继承关键点
//生成实例
var child = new SubClass()
child.age// 123
child.name // undefined

注释:

  • 子构造函数的原型 = 父构造函数的原型实现了原型继承
  • 基于子构造函数new一个新的实例对象,该实例对象间接继承了父构造函数的原型属性
  • 所以打印:child.age // 18
  • 后续大家也能看到,打印了 child.name // 输出 undefined ,很简单,子构造函数单单只继承了父构造函数的原型属性跟方法,并没有继承父构造函数自己的属性跟方法,所以 undefined ,那有没有办法同时也继承父构造函数自己的属性跟方法呢 ? 答案当然是有的,接着往下看

原型链继承

//即 子构造函数.prototype = new 父构造函数()
// 创建父构造函数
function SuperClass(){
    this.name = 'HiSen';
    this.age = 25;
    this.showName = function(){
        console.log(this.name);
    }
}
// 设置父构造函数的原型属性  friends 及方法 showAge
SuperClass.prototype.friends = ['js', 'css'];
SuperClass.prototype.showAge = function(){
    console.log(this.age);
}
// 创建子构造函数
function SubClass(){}
// 实现继承 - 子构造函数的原型 = new 一个父构造函数
SubClass.prototype = new SuperClass();
// 修改子构造函数的原型的构造器属性,因为此时继承了父构造函数指向 SuperClass; 所以要修改一下。
SubClass.prototype.constructor = SubClass;

//生成实例
var child = new SubClass();

console.log(child.name); // HiSen
console.log(child.age);// 25
child.showName();// HiSen
child.showAge();// 25
console.log(child.friends); // ['js','css']

// 当我们改变friends的时候, 父构造函数的原型对象的也会变化
child.friends.push('html');
console.log(child.friends);// ["js", "css", "html"]

var father = new SuperClass();
console.log(father.friends);// ["js", "css", "html"]

通过这个例子可以看到,我们子构造函数不仅继承了父构造函数原型上的属性跟方法,而且还继承了父构造函数自己的属性跟方法;细心的童鞋可能已经看到了痛点,最后我们修改子构造函数继承的属性的时候,父构造函数的原型对象也受到了影响,那有没有办法解决一下这个痛点呢?emmm ~~ 方法必须有,比如:

Object.create

ECMAScript 5 中引入了一个新方法:Object.create()。可以调用这个方法来创建一个新对象。新对象的原型就是调用 create 方法时传入的第一个参数:

var obj = {a:1,b:2,c:3}
var obj2 = Object.create(obj)
// obj2.__proto__  返回 {a:1,b:2,c:3}
// obj2.a // 1

Object.create() 方法接受两个参数:Object.create(obj,propertiesObject) ; obj: 一个对象,应该是新创建的对象的原型。 propertiesObject:可选。该参数对象是一组属性与值,该对象的属性名称将是新创建的对象的属性名称,值是属性描述符(这些属性描述符的结构与Object.defineProperties()的第二个参数一样)。注意:该参数对象不能是 undefined,另外只有该对象中自身拥有的可枚举的属性才有效,也就是说该对象的原型链上属性是无效的。

当然还有其它的方式,像使用 深拷贝完全复制, Object.assign() , ES6 的 class 类 等都是可以实现继承的;这里就不一一举例了。

Js 执行机制 - 事件循环 - Event Loop

扩展:单线程

Javascript 的单线程 - 引用思否的说法: JavaScript的一个语言特性(也是这门语言的核心)就是单线程。什么是单线程呢?简单地说就是同一时间只能做一件事,当有多个任务时,只能按照一个顺序一个完成了再执行下一个。

扩展:为什么JS是单线程

JS最初被设计用在浏览器中,作为浏览器脚本语言,JavaScript的主要用途是与用户互动以及操作DOM,如果浏览器中的JS是多线程的,会带来很复杂的同步问题

比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准 ?

所以为了避免复杂性,JavaScript从诞生起就是单线程,为了提高CPU的利用率,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以这个标准并没有改变JavaScript单线程的本质;

事件循环

在Js执行中,同步任务和异步任务分别进入不同线程,同步的进入主线程,异步的进入 Event Table (事件列表) 并注册函数 。 当指定的异步任务完成时,Event Table 会将这个函数移入 Event Queue (事件队列)。 当主线程内的任务执行为空时,会去 Event Queue (任务队列) 读取对应的函数,进入主线程执行。 上述过程会不断重复,也就是我们常说的 Event Loop (事件循环) 。通过一张图看一下 在这里插入图片描述 接下来我们通过代码例子来实践一下:

  setTimeout(function(){
  	console.log('1')
  });
  new Promise(function(resolve){
      console.log('2');
      resolve();
  }).then(function(){
      console.log('3')
  });
  console.log('4');    

注释:根据 Js 单线程,自上而下执行 首先 setTimeout 是异步进入 事件队列,然后 promise 的 then 也是异步进入事件队列 ,

那么按照我们上边说的Js执行机制,先走主线程的同步任务,打印2 ,然后4,紧跟着执行异步任务,也就是任务队列打印1,随后是 3 , 所以结果应该是 2,4,1,3, 事实真的是这样子嘛 ? 接着往下看 :

我们上边的 setTimeout 放到了 event queue 事件队列里 , promise 的 then 函数 也被放到了 event queue 事件队列里,然而杯具来了,这两个 queue 并不是一个队列;

--------------重要 start--------------- 在 Js Event Loop 机制中 Promise 执行器中的代码会被主线程同步调用,但是 promise 的回调函数是基于微任务的 宏任务的优先级高于微任务 看细节,通俗一点就是微任务高于宏任务, 下边两句话 每一个宏任务执行完毕前都必须先将当前的微任务队列清空 所以也可以理解为微任务的优先级高于宏任务 emmmm ~~ --------------重要 end----------------

Js 中的宏任务和微任务 - 略记一下

macro-task(宏任务) :包括整体代码 script,setTimeout,setInterval micro-task(微任务) : Promise,process.nextTick

现在我们回到上边的例子中,因为 settimeout 是宏任务,虽然先执行的它,但是他被放到了宏任务的 event queue 里面,然后代码继续往下检查看有没有微任务,检测到 Promise 的 then 函数把它放入了微任务队列。等到主线进程的所有代码执行结束后。先从微任务 queue 里拿回调函数,然后微任务queue空了后再从宏任务的queue拿函数。

所以正确的执行结果当然是:2,4,3,1 ;

函数防抖与节流

其实函数防抖和节流都是对web性能的优化方案,主要是对网页中频繁触发,频率较快的事件进行一个优化。例如:滚动条事件,输入框输入事件,鼠标移动事件等。

防抖

函数防抖其实是在规定时间内,频繁触发该事件,以最后一次触发为准; 实现方式就是创建一个定时器,每次执行的时候,清除旧定时器,并创建一个新的定时器重新记录时间

//参数:要执行的函数和间隔的毫秒数
function debounce(fn, time) {
   var timer = null; // 声明 timer 
    return function() {
    clearTimeout(timer) // 清除定时器
    timer = setTimeout(function() { // 创建定时器赋给局部变量 timer 
            fn.apply(this)
        }, time)
    }
}
// 在规定时间内,触发多次,以最后那一次为准,
// 因为每一次触发,旧的定时还没有执行就被清除了,又创建了新的定时器

节流

函数节流其实是在规定时间内,事件只被触发一次 ; 实现方式就是通过时间戳,当前的时间戳 - 最后一次执行的时间戳 > 设置规定时间,则生效一次;也就说在频繁触发的情况下,该事件触发的频率会降低。

// 参数:要执行的函数和毫秒数
function throttle(fn, time) {
    var lastTime = 0; // 初始化最后一次执行时间
    return function() {
        var nowTime = Date.now(); // 获取当前时间毫秒数 
        if (nowTime - lastTime > time) { // 当前时间毫秒数 - 最后一次执行时间毫秒数 > 设置规定时间
            fn.call(this);
          lastTime = nowTime; // 更新最后一字执行时间毫秒数
        }
    }
}
// 每一次执行,当前时间就会减去最后一次执行时间
// 如果大于设置时间,就触发一次,有效的节省了事件触发频率。

函数柯里化

函数柯里化简单的理解可以是:把函数多个参数转换成单个参数的链式调用,其实有点像分段返回,我们通过一个简单的例子来了解一下

// 普通方式
function add (x, y, z) {
    return x + y + z;
}
add(1, 2, 3) // 6
// 柯里化方式
function currying (x) {
	console.log("x = " + x) // do something
    return function(y) {
        console.log("y = " + y) // do something
    	return function(z) {
			 return x + y + z
		}
    }
}
currying(1)(2)(3)  // 6  
// x = 1
// y = 2
// 6 

从这个简单的例子可以看出柯里化的好处展现的淋漓尽致 我分别在不同的位置打印了对应的值,结果显而易见,相当于分段返回当前结果,那可以做些什么事情呢 ? 就不言而喻了,比如:我们需要通过前两个参数的结果去做一件事情,然后还需要使用最后的结果做一件事情,是不是就可以分别触发对应的函数了。

欢迎点赞,小小鼓励,大大成长