JavaScript系列-4-函数进阶

本文作者:钟昕灵,叩丁狼高级讲师。原创文章,转载请注明出处。

作用域安全的构造函数

构造函数的调用方式存在下面两种:

​ 直接调用:普通函数

​ 使用new一起调用:创建对象

function Person(name, age) {
    
    
    this.name = name;
    this.age = age;
}
console.log(Person("zs", 10));//undefined
console.log(new Person("ls", 12));//初始化了name和age的Person对象

如果我们直接调用Person函数,因为函数默认的返回值为undefined,所以得到undefined结果。

如果使用new关键字来调用Person函数,此时在函数中会默认创建一个对象,并将该对象设置给this,然后将name和age封装到该对象中,最后返回该对象。所以得到的是一个封装好数据的对象。

所以,如果我们想要创建对象,这里必须使用new来调用。但是,在实际来发中,我们有可能会忘记使用new,而是直接调用该构造函数,此时会造成什么问题呢?

  1. 得不到想要的对象,这是最容易想到的

  2. 会存在作用域安全的问题,这里需要解释一下

    直接调用该函数,那么在函数内部中的this指向window

    如果我们在函数中需要修改当前创建对象(this)中的属性时,有可能会不知不觉的将window作用域下的某些变量给修改掉,导致数据错乱的问题。

    扫描二维码关注公众号,回复: 13342138 查看本文章
var name = "今天天气不错";
function Person(name, age) {
    
    
    this.name = name;
    this.age = age;
}
Person("zs", 10);
console.log(name);//zs

此时,在Person函数中的this是指向window的,所以this.name访问到的是函数外面(全局作用域)中的name,并为其赋值为zs,所以,最终得到的name值为zs。

既然存在这样的问题,我们就得解决,那么思路应该是怎样的呢?

首先,造成上面问题的根本原因是程序员在使用的过程中可能会忘记new关键字,而导致作用域不安全的问题

所以,而当没有使用new关键字的时候,构造函数中的this关键字是指向window的

反过来,如果构造函数中的this指向window,说明没有使用new关键字,此时就有了下面的代码:

function Person(name, age) {
    
    
    if(this == window){
    
    
        throw "调用构造器需要使用new关键字";
    }else{
    
    
        this.name = name;
        this.age = age;
    }
}

在构造函数中判断this的指向即可解决忘记new关键字的问题。

但是在ES6中,这种方式存在一定的问题,此时的this不一定是指向window,原因我们后面再说。此时我们换种思路来解决。

如果使用new调用该构造函数,那么this指向的是什么呢?对,是当前构造函数创建的对象,所以根据类型判断也是可以的。

if(!(this instanceof Person)){
    
    
    throw "调用构造器需要使用new关键字";
}else{
    
    
    this.name = name;
    this.age = age;
}

上面这种方式是完全OK的,下面我们再给出一种方式,大家可以了解一下。

在ES6中,为new引入了一个target属性,如果没有使用new调用构造函数,那么在该构造函数中new.target为undefined,反之为当前的构造函数。

if(!new.target){
    
    
    throw "调用构造器需要使用new关键字";
}else{
    
    
    this.name = name;
    this.age = age;
}

以上解决了我们在使用构造函数创建对象的过程中可能存在的问题。如果出现了,我们也能够快速的解决。

Function和Object

前面我们学习了Function和Object,而且也学习了instanceof关键字的使用,下面来看几个例子,检验一下大家对前面所学知识点的掌握情况。

function Person() {
    
    
}
var p = new Person();
console.log(p instanceof Person);//①
Person.prototype = {
    
    };//修改Person的原型对象
console.log(p instanceof Person);//②

①处的打印结果相信大家都非常清楚,因为p对象是由Person构造函数创建出来的,所以Person构造函数的原型对象在p对象的原型链上,所以使用instanceof判断的结果为true。

②处的结果会受到上面修改Person原型对象的影响,修改之后Person的原型对象不在p的原型链中,所以结果返回false。

var f = new Function();
console.log(f instanceof Function);//①
console.log(f instanceof Object);//②
console.log(Function instanceof Object);//③
console.log(Object instanceof Function);//④

①:返回true

②:返回true

上面两个比较简单,这里就不再做说明了。

③:这个也比较简单,Object的原型对象是所有对象的原型链的终点,所以只要最后是Object,都应该返回true。

④:这个判断稍微有点难度,但是如果大家对于前面画过的原型链的图还熟悉的话,应该能够得到正确答案。

因为Function.prototype是Object这个函数对象的原型对象,所以这句话可以这样说了,Function的原型对象在Object的原型链上,所以该判断理应返回true。

总结:如果大家能比较快的得到上面每个练习的答案的话,说明大家对于instanceof和对象的原型链还是认识的比较透彻了,恭喜大家!

浅拷贝和深拷贝的实现

在开发中,我们会有这样的需求,就是将A对象中的属性或者是方法拷贝到B对象中,而这里的拷贝我们按照拷贝的深度分为浅拷贝和深拷贝,下面我们来分析一下:

var p1 = {
    
    
    name:"zs",
    age:10,
    favs:["H5","Java","C"],
    wife:{
    
    
        name:"lily",
        age:8
    }
}
var p2 = {
    
    };
for(var key in p1){
    
    
    p2[key] = p1[key];
}
console.log(p2);

上面的代码中,我们将p1对象中的属性拷贝给了p2对象,这种拷贝方式我们称之为浅拷贝,为什么呢?我们来画图说明。

叩丁狼教育.png

上面是p1对象的内存结构图,通过上面的拷贝操作得到的p2是什么结构呢?

叩丁狼教育.png

我们得到和0x11一模一样的一份数据,而p2就指向该内存区域的数据,然后在0x44中的favs和wife这两个属性仍然指向0x22和0x33这两块内存区域的数据,所以此时的拷贝只拷贝了对象中的第一层属性,称之为浅拷贝。

浅拷贝在使用的过程中存在数据共享的问题(如果修改p1中的favs或者wife中的数据,p2中的这两个属性也会跟着被修改),因为他们引用的是同一块内存区域的数据。这个问题的我们可以使用深拷贝来实现。

所谓深拷贝,就是将对象引用的对象,或者对象引用的对象的引用的对象,一次往下推,全部都拷贝,大家不共享任何数据。

所以要实现深拷贝,当我们发现属性对应的值是一个对象的时候,应该将该对象拷贝一份,然后赋值给当前属性。

function deepCopy(source,target) {
    
    
    for(var key in source){
    
    
        if(source.hasOwnProperty(key)){
    
    //只拷贝当前对象的属性
            if(typeof source[key] == "object"){
    
    //如果属性是引用类型的对象
                // //根据原属性的类型决定是数组还是普通对象
                target[key] = Array.isArray(source[key]) ? [] : {
    
    };
                deepCopy(source[key],target[key]);//递归调用,完成所有层次的拷贝
            }else{
    
    
                target[key] = source[key];
            }
        }
    }
}
deepCopy(p1,p2);

通过上面的深度拷贝得到的p2对象是和p1完全不同的两份数据,此时不再存在数据共享的问题。

叩丁狼教育.png

函数的调用和this的丢失

调用函数大家都非常熟悉了,这里再统一的复习总结一下,需要强调的是,使用不同的方式调用函数,函数内部的this指向存在不同

  1. 普通调用 fun() this指向调用函数的对象—window
  2. 对象调用 obj.fun() this指向调用函数的对象—obj
  3. 使用new关键字调用 new Fun() this指向函数内部创建的新对象
  4. call或者apply调用 this指向call或者apply方法的第一个参数

所以我们在调用函数的过程中需要时刻关注我们调用方式的不同对this的影响,如下面的案例中就发生了this的丢失问题。

<div id="main"></div>
<script>
    console.log(document.getElementById("main"));
    var getById = document.getElementById;
    console.log(getById("main"));//Uncaught TypeError: Illegal invocation
</script>

根据元素id属性获取元素的操作是大家非常熟悉的,而这种代码也是非常需要优化的,功能很小但是代码太长。

所以,上面的代码中将根据元素id获取元素的方法赋值给了另外一个变量getById,此时的getById就等价于document.getElementById方法,所以,按理说,应该可以使用getById完成获取元素的操作,但是结果却报错了,这是为什么呢?

原因其实很简单:

  1. 在document的getElementById方法中用到了this,正常使用document调用的时候this是指向document的
  2. 要完成获取元素的功能,this就必须要指向document
  3. 当使用getById(“main”)调用函数的时候,函数中this拿到的确实window
  4. 函数中本来需要的是document拿到的确实window的时候,代码执行报错

那么,如果非得对这段代码做优化,我们应该怎么做呢?

var getById = function(id){
    
    
    return document.getElementById(id);
}
console.log(getById("main"));

这段代码大家相信大家都能够看懂,所以我们以后再简化一个函数的使用的时候,一定要注意,不要轻易的将函数的this给搞丢了。

深入理解函数对象(函数的创建和名称)

创建一个函数也是存在多种方式,这里我们来总结一下:

//方式一:
function f1() {
    
    
}
//方式二:
var f2 = function () {
    
    
}
//方式三:
var f3 = function f4() {
    
    
}
//方式四:
var f5 = new Function();

方式一:最普通的语法结构创建函数

方式二:创建一个匿名函数,并将该函数赋值给一个变量

方式三:创建一个命名函数,需要注意的是,此时只能使用f3调用函数,f4不行,f4这个名称只能在函数内部使用

方式四:使用指定的模板创建函数

通过前面对Function对象的学习我们了解到,每个函数都有name属性可以获取到函数的名称

console.log(f1.name);//f1
console.log(f2.name);//f2
console.log(f3.name);//f4
console.log(f5.name);//anonymous

anonymous说明f5这是一个匿名函数,而f2这个函数本应该也是一个匿名函数,这是不同浏览器版本的差异问题,这个大家了解即可。

深入理解函数对象(函数作为参数和返回值)

函数作为参数

将函数作为另一个函数的返回值,在JS中经常出现,那么我们来看看这两种操作需要注意的点。

function fun(callback) {
    
    
    console.log("我被调用了");
    callback();
}
fun(function () {
    
    
    console.log("我是回调函数");
});

fun函数的参数是函数类型的,所以在调用的时候传入另一个函数,并在fun中调用,此时传递进来的函数称之为回调函数。

回调函数只有在满足一定的业务需求之后才会被调用执行,否则不会执行。

function fun(callback) {
    
    
    console.log("我被调用了");
    callback();
}
var obj = {
    
    
    name:"Neld",
    doWork : function () {
    
    
        console.log(this.name);
    }
};
fun(obj.doWork);//空字符串

结合上面this丢失的问题一起来分析上面的代码,为什么输出的结果是空字符串呢?

首先我们是将obj中的doWork函数作为参数传入了fun函数并调用执行。

如果doWork函数是使用obj调用的,那么函数中的this指向的就是obj,得到结果应该为Neld。

但现在该函数是在fun中一普通函数的方式被调用,所以此时的this指向window,访问到的name是window中的name属性,值为空字符串。

所以,随着我们对JS的深入理解,很多的问题我们都能够比较快的分析出来了。

如果需要修改doWork函数中this的指向,使用call或者apply方法即可实现。

函数作为返回值

function fun() {
    
    
    var a = 123;
    return function () {
    
    
        console.log(a);
    }
}
var f = fun();
f();//123

fun函数的返回值是另一个函数,调用fun函数之后得到该返回的函数,然后再对该函数进行操作。

上面所用到的就是JS中的闭包,关于闭包的内容在我们后面的课程中会重点介绍,敬请期待。

那么利用这种特性,能够为我们解决什么问题呢?往下看:

计数器案例:

var i = 1;
function count() {
    
    
    return i++;
}
console.log(count());//1
console.log(count());//2
console.log(count());//3
console.log(count());//4

上面的代码实现了计数器的功能,没调用一次count函数,i的值增加1。

当时这种方式存在很大的问题,i是全局变量,容易被修改。

var i = 1;
function count() {
    
    
    return i++;
}
console.log(count());//1
console.log(count());//2
i = 100;
console.log(count());//100
console.log(count());//101

由于i是全局变量造成的该问题,那么解决思路很清晰,降低i的作用域即可。

function count() {
    
    
    var i = 1;
    return i++;
}

是这样吗?很显然不是,此时的i会在每次调用count的时候被初始化为1,所以达不到计数的效果。

function count() {
    
    
    var i = 1;
    return function () {
    
    
        return i++;
    };
}
var ret = count();
console.log(ret());//1
console.log(ret());//2
i = 100;
console.log(ret());//3
console.log(ret());//4

count函数的返回值是一个函数,在该函数中访问外部函数的私有成员i,对其做自增操作。

调用count之后,获取到的是内部返回的函数对象,所以再调用这个函数对象,执行其中的自增操作即可。

由于i是作为count函数的私有成员,所以在函数外部无法访问,也就无法被修改,安全!

上面代码还可以做进一步的优化:

var ret = (function() {
    
    
    var i = 1;
    return function () {
    
    
        return i++;
    };
})();

在定义一个函数后,如果需要立即而且只执行一次的话,可以使用 **(函数对象)()**的语法结构,上面就是利用这种方式,立即执行函数将返回值(另一个函数)赋值给ret变量。

函数使用的典型结构(IIFE立即执行函数)

Immediately-Invoked Function Expression (IIFE):立即执行函数或者即时函数

这种函数就是上一节我们看到的效果,就是需要在函数定义之后立即执行一次,并且只能执行这一次,以后不能再调用,通常使用在初始化的阶段。

立即执行函数可以使用下面的语法来实现:

(function () {
    
    
    console.log("方式一");
})();
(function () {
    
    
    console.log("方式二");
}());
!function () {
    
    
    console.log("方式三");
}();
+function () {
    
    
    console.log("方式四");
}();
-function () {
    
    
    console.log("方式五");
}();
~function () {
    
    
    console.log("方式六");
}();

题外话:函数,括号,语法错误

有趣的是,如果你为一个函数指定了名称并且在立刻在其后边放置了括号,解析器也会抛出错误,但原因不同。虽然在表达式之后放置括号说明这是一个将被执行的函数,但在声明之后放置括号会与前面的语句分离,成为一个分组操作符(可以作为优先提升的方法)。

// 现在这个函数声明的语法是正确的,但还是有报错
// 表达式后面的括号是非法的, 因为分组运算符必须包含表达式
function foo(){
    
     /* code */ }(); // SyntaxError: Unexpected token )
// 如果你在括号内放置了表达式, 没有错误抛出...
function foo(){
    
     /* code */ }( 1 );
// 但是函数也不会执行,因为它与一个函数声明后面放一个完全无关的表达式是一样的
function foo(){
    
     /* code */ }
( 1 );

函数使用的典型结构(惰性函数定义)

惰性函数:在函数中会进行一些分支判断或者初始化更新操作,然后将函数修改函数的指向,那么再次调用该函数的时候,执行的是修改之后指向的函数。

function foo() {
    
    
    console.log("初始化操作");
    foo = function () {
    
    
        console.log("真正的业务逻辑处理");
    }
}
foo();//初始化操作
foo();//真正的业务逻辑处理
foo();//真正的业务逻辑处理

在foo函数中,我们可以先执行初始化相关的操作,然后将另外一个函数赋值给foo,所以再次调用foo的时候,执行的是对真实业务逻辑的处理操作。

惰性函数在JS中使用比较多,但是需要注意下面几点:

  1. 函数对象中的属性在更新之后会丢失

    foo.des = "描述信息";
    console.log(foo.des);//描述信息
    foo();//初始化操作
    foo();//真正的业务逻辑处理
    foo();//真正的业务逻辑处理
    console.log(foo.des);//undefined
    

    因为函数更新之后指向的是另外一个函数对象,在该函数对象中没有des这个属性。

  2. 如果将惰性函数赋值给一个变量,通过这个变量调用该函数,此时无法执行到更新之后的函数

    function foo() {
          
          
        console.log("初始化操作");
        foo = function () {
          
          
            console.log("真正的业务逻辑处理");
        }
    }
    var f = foo;
    f();//初始化操作
    f();//初始化操作
    f();//初始化操作
    

    因为调用f函数后,只是修改了foo指向另一个函数,而f仍然指向之前的函数,没有改变。

叩丁狼教育.png

作用域简单介绍

函数和变量声明的提升

在JS中存在一个很重要的特性,函数和变量声明的提升,理解这一点对于理解我们编写的代码非常有帮助,那么声明是声明的提升呢?我们通过下面的代码来分析。

console.log(a);//①
var a = 123;
console.log(a);//②

console.log(f);//③
f();//④
function f() {
    
    
    console.log("函数声明提升");
}

①处的代码如果按照我们以前的理解,代码从上而下执行,那么在执行这行代码的时候,a还没有被声明,所以直接访问一个没有被声明的变量,程序应该报错。

但是结果却大出所料,这里得到的结果是undefined。

③处的结果也和我们最初的认识是不一样的,结果为f对应的函数对象。

造成这个结果是因为变量和函数的作用域提升的原因,什么意思呢?

JS是解释性语言,JS引擎对代码的处理分为两步:

​ 预解析处理:在代码执行之前,对代码做全局扫描,对代码中出现的变量或者函数提前声明处理;

​ 解析之后我们的代码:

var a;//提前声明,但不初始化
console.log(a);//undefined
a = 123;
console.log(a);//123

//提前声明
function f() {
    
    
    console.log("函数声明提升");
}
console.log(f);//函数对象
f();//函数声明提升

​ 具体的执行:自上而下的执行代码

如果有这样的认识的话,相信大家就能够顺利的分析出上面程序的正确结果了。

变量提升和作用域的关系

下面给出两个练习帮助大家理解变量提升和作用域的关系。

题1:

f();
function f() {
    
    
    console.log("1");
}
f();
function f() {
    
    
    console.log("2");
}
f();
function f() {
    
    
    console.log("3");
}

根据前面对函数声明提升的认识,相信大家能够得出这里三个打印的正确结果,对的,三次都是 “3”。

预解析之后的代码:

function f() {
        console.log("3");
}
f();
f();
f();

为什么解析之后只剩下一个函数,而且是最后那一个?

因为三个函数的名称一样,后面的函数会将前面的覆盖,所以最后只剩下最后一个函数了。

题2:

console.log(a);
var a = 123;
console.log(a);
function f1() {
    
    
    console.log(a);
    var a = 456;
    console.log(a);
}
f1();
console.log(a);

不废话,直接先对代码做预解析,然后再做分析。

var a;//变量声明提升
function f1() {
    
    //函数声明提升
	var a;//变量声明提升
    console.log(a);
    a = 456;
    console.log(a);
}
console.log(a);
a = 123;
console.log(a);
f1();
console.log(a);

解析得到上面的代码结果就非常明显了,分别是:undefined 123 undefined 456 123

由于在函数内部有变量a,所以在函数中访问到的是这个局部变量,如果在函数作用域中没有变量a,那么就会跳出函数作用域来到全局作用域来查找。

声明提升的规则

声明提升是将变量或者函数的声明提升到当前作用域的最顶端。在具体使用的过程中存在以下需要注意的细节。

  1. 变量和变量同名,解析之后只存在一个当前变量的声明
console.log(a);
var a = 123;
console.log(a);
var a = 456;
console.log(a);

解析之后:

var a;
console.log(a);//undefined
a = 123;
console.log(a);//123
a = 456;
console.log(a);//456
  1. 函数和函数同名,后面的声明将前面的覆盖
f();
function f() {
    
    
    console.log("1");
}
f();
function f() {
    
    
    console.log("2");
}
f();
function f() {
    
    
    console.log("3");
}

解析之后:

function f() {
    
    
    console.log("3");
}
f();//3
f();//3
f();//3
  1. 函数和变量同名,函数声明提升,忽略变量的声明
console.log(a);
var a = 123;
console.log(a);
function a() {
    
    }
console.log(a);
function a() {
    
    }
console.log(a);

解析之后:

function a() {
    
    }
console.log(a);//函数a
var a = 123;//将前面的函数覆盖,a的值变为123
console.log(a);//123
console.log(a);//123
console.log(a);//123
  1. 如果是命名函数,则只将前面的变量声明提升,函数不动。
console.log(fn1);
function fn1() {
    
    
}
console.log(fn1);
console.log(fn2);
var fn2 = function () {
    
    
}
console.log(fn2);

解析之后:

function fn1() {
    
    
}
var fn2;
console.log(fn1);//fn1函数
console.log(fn1);//fn1函数
console.log(fn2);//undefined
fn2 = function () {
    
    
}
console.log(fn2);//fn2函数

作用域链和访问规则

作用域链中存储了当前所在的作用域和能够访问到的作用域,如果我们要访问的变量不在当前作用域链中,那么程序会报错,反之从链上对应的作用域中获取到变量。

那么在作用域链中的成员访问规则是怎么样的呢?

function f1() {
    
    
    var a = 1;
    function f2() {
    
    
        var b = 2;
        function f3() {
    
    
            var c = 3;
            function f4() {
    
    
                var d = 4;
                console.log(a,b,c,d);//1 2 3 4
            }
            f4();
        }
        f3();
    }
    f2();
}
f1();

上面定义了4个函数,他们层层嵌套,我们需要知道的是,每个函数中变量的作用域及其访问规则。

函数中定义的变量称为局部变量,它只属于当前函数的作用域及其嵌套函数的作用域中,外界无法访问。也就是一种由内而外的访问,反之则不行。

叩丁狼教育.png

闭包相关知识点复习

JS中每个函数都有自己的作用域,在当前函数中定义的变量,只能在该函数或该函数的嵌套函数中访问。

如:

function fun(){
    
    
    var a = 123;
    console.log(a);//123
}
console.log(a);//报错:a没有定义

在开发中,如果需要在当前函数作用域外访问函数的局部变量能实现吗?看下面的代码:

function fun() {
    
    
    var a = 123;
    function fun2() {
    
    
        return a;
    }
    return fun2;
}
var fun3 = fun();
console.log(fun3());//123

在函数fun中定义了另一个函数fun2,fun2中访问函数外部的局部变量a,最后将fun2返回。

调用fun得到返回值(fun2函数),因为该返回值是一个函数,所以我们调用得到函数中返回的a的值。

这种在函数中返回另一个函数的结构就称之为闭包,闭包在的主要作用就是打破函数的作用域的限制,能在需要的地方访问函数内部的私有成员。

闭包访问和设置数据

上面我们通过闭包,能够访问到一个函数中的私有成员,那么如果需要访问其中的多个私有成员,应该如何实现呢?

这里大家应该能够想到,函数的返回值只能有一个,如果有多个值需要返回,我们完全可以使用数组来实现。

function fun() {
    
    
    var a = 123;
    var b = 456;
    return function() {
    
    
        return [a, b];
    }
}
var f = fun();
var a = f()[0];
var b = f()[1];
console.log(a, b);//123 456

也或者是:

function fun() {
    
    
    var a = 123;
    var b = 456;
    return [
        function() {
    
    
            return a;
        },
        function() {
    
    
            return b;
        }
    ];
}
var f = fun();
var a = f[0]();
var b = f[1]();
console.log(a, b);//123 456

上面两种方式都是使用数组实现访问多个私有成员的需求,在实际开发中不太好使,因为数组是使用所有来访问元素,如果数组中元素个数较多,那么这种方式就容易出错了。所有我们想到使用对象的形式来返回。

function fun() {
    
    
    var a = 123;
    var b = 456;
    return {
    
    
        getA:function () {
    
    
            return a;
        },
        getB:function () {
    
    
            return b;
        }
    };
}
var f = fun();
var a = f.getA();
var b = f.getB();
console.log(a, b);

上面这种方式就好多了,将需要访问的函数定义到对象中,然后通过访问对象的属性来达到目的。

如此一来,我们也可以将访问变量的其他方法都定义在该对象中,如,为变量赋值的方法。

function fun() {
    
    
    var a = 123;
    var b = 456;
    return {
    
    
        getA:function () {
    
    
            return a;
        },
        getB:function () {
    
    
            return b;
        },
        setA:function (value) {
    
    
            a = value;
        },
        setB:function (value) {
    
    
            b = value;
        }

    };
}
var f = fun();
f.setA(100);
f.setB(200);
var a = f.getA();
var b = f.getB();
console.log(a, b);//100 200

这是在以后开发中经常使用到的,大家可以多熟悉一下。

最后,使用前面学过的知识点来对上面的代码做一次优化。

var modual = (function () {
    
    
    var a = 123;
    var b = 456;
    function getA() {
    
    
        return a;
    }
    function getB() {
    
    
        return b;
    }
    function setA(value) {
    
    
        a = value;
    }
    function setB(value) {
    
    
        b = value;
    }
    return {
    
    
        getA:getA,
        getB:getB,
        setA:setA,
        setB:setB
    };
})();//立即执行函数,返回内部的封装的对象
modual.setA(100);
modual.setB(200);
var a = modual.getA();
var b = modual.getB();
console.log(a, b);//100 200

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/wolfcode_cn/article/details/89405014
今日推荐