首页 > 技术文章 > H5前端技术文章 >

JavaScript系列-4-函数进阶

更新时间:2020-06-08 | 阅读量(1,239)

>本文作者:钟昕灵,叩丁狼高级讲师。原创文章,转载请注明出处。 ## 作用域安全的构造函数 构造函数的调用方式存在下面两种: ​ 直接调用:普通函数 ​ 使用new一起调用:创建对象 ```js 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作用域下的某些变量给修改掉,导致数据错乱的问题。 ```js 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关键字,此时就有了下面的代码: ```js function Person(name, age) { if(this == window){ throw "调用构造器需要使用new关键字"; }else{ this.name = name; this.age = age; } } ``` 在构造函数中判断this的指向即可解决忘记new关键字的问题。 但是在ES6中,这种方式存在一定的问题,此时的this不一定是指向window,原因我们后面再说。此时我们换种思路来解决。 如果使用new调用该构造函数,那么this指向的是什么呢?对,是当前构造函数创建的对象,所以根据类型判断也是可以的。 ```js if(!(this instanceof Person)){ throw "调用构造器需要使用new关键字"; }else{ this.name = name; this.age = age; } ``` 上面这种方式是完全OK的,下面我们再给出一种方式,大家可以了解一下。 在ES6中,为new引入了一个target属性,如果没有使用new调用构造函数,那么在该构造函数中new.target为undefined,反之为当前的构造函数。 ```js if(!new.target){ throw "调用构造器需要使用new关键字"; }else{ this.name = name; this.age = age; } ``` 以上解决了我们在使用构造函数创建对象的过程中可能存在的问题。如果出现了,我们也能够快速的解决。 ## Function和Object 前面我们学习了Function和Object,而且也学习了instanceof关键字的使用,下面来看几个例子,检验一下大家对前面所学知识点的掌握情况。 ```js 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。 ```js 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对象中,而这里的拷贝我们按照拷贝的深度分为浅拷贝和深拷贝,下面我们来分析一下: ```js 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对象,这种拷贝方式我们称之为浅拷贝,为什么呢?我们来画图说明。 ![image.png](https://upload-images.jianshu.io/upload_images/11387429-2d871a473a9de6a4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 上面是p1对象的内存结构图,通过上面的拷贝操作得到的p2是什么结构呢? ![image.png](https://upload-images.jianshu.io/upload_images/11387429-1273dcdd258b6082.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 我们得到和0x11一模一样的一份数据,而p2就指向该内存区域的数据,然后在0x44中的favs和wife这两个属性仍然指向0x22和0x33这两块内存区域的数据,所以此时的拷贝只拷贝了对象中的第一层属性,称之为浅拷贝。 浅拷贝在使用的过程中存在数据共享的问题(如果修改p1中的favs或者wife中的数据,p2中的这两个属性也会跟着被修改),因为他们引用的是同一块内存区域的数据。这个问题的我们可以使用深拷贝来实现。 所谓深拷贝,就是将对象引用的对象,或者对象引用的对象的引用的对象,一次往下推,全部都拷贝,大家不共享任何数据。 所以要实现深拷贝,当我们发现属性对应的值是一个对象的时候,应该将该对象拷贝一份,然后赋值给当前属性。 ```js 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完全不同的两份数据,此时不再存在数据共享的问题。 ![image.png](https://upload-images.jianshu.io/upload_images/11387429-b471c83cfef51d93.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## 函数的调用和this的丢失 调用函数大家都非常熟悉了,这里再统一的复习总结一下,需要强调的是,使用不同的方式调用函数,函数内部的this指向存在不同 1. 普通调用 fun() this指向调用函数的对象---window 2. 对象调用 obj.fun() this指向调用函数的对象---obj 3. 使用new关键字调用 new Fun() this指向函数内部创建的新对象 4. call或者apply调用 this指向call或者apply方法的第一个参数 所以我们在调用函数的过程中需要时刻关注我们调用方式的不同对this的影响,如下面的案例中就发生了this的丢失问题。 ```html

 

``` 根据元素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的时候,代码执行报错 那么,如果非得对这段代码做优化,我们应该怎么做呢? ```js var getById = function(id){ return document.getElementById(id); } console.log(getById("main")); ``` 这段代码大家相信大家都能够看懂,所以我们以后再简化一个函数的使用的时候,一定要注意,不要轻易的将函数的this给搞丢了。 ## 深入理解函数对象(函数的创建和名称) 创建一个函数也是存在多种方式,这里我们来总结一下: ```js //方式一: function f1() { } //方式二: var f2 = function () { } //方式三: var f3 = function f4() { } //方式四: var f5 = new Function(); ``` 方式一:最普通的语法结构创建函数 方式二:创建一个匿名函数,并将该函数赋值给一个变量 方式三:创建一个命名函数,需要注意的是,此时只能使用f3调用函数,f4不行,f4这个名称只能在函数内部使用 方式四:使用指定的模板创建函数 通过前面对Function对象的学习我们了解到,每个函数都有name属性可以获取到函数的名称 ```js console.log(f1.name);//f1 console.log(f2.name);//f2 console.log(f3.name);//f4 console.log(f5.name);//anonymous ``` anonymous说明f5这是一个匿名函数,而f2这个函数本应该也是一个匿名函数,这是不同浏览器版本的差异问题,这个大家了解即可。 ## 深入理解函数对象(函数作为参数和返回值) **函数作为参数** 将函数作为另一个函数的返回值,在JS中经常出现,那么我们来看看这两种操作需要注意的点。 ```js function fun(callback) { console.log("我被调用了"); callback(); } fun(function () { console.log("我是回调函数"); }); ``` fun函数的参数是函数类型的,所以在调用的时候传入另一个函数,并在fun中调用,此时传递进来的函数称之为回调函数。 回调函数只有在满足一定的业务需求之后才会被调用执行,否则不会执行。 ```js 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方法即可实现。 **函数作为返回值** ```js function fun() { var a = 123; return function () { console.log(a); } } var f = fun(); f();//123 ``` fun函数的返回值是另一个函数,调用fun函数之后得到该返回的函数,然后再对该函数进行操作。 上面所用到的就是JS中的闭包,关于闭包的内容在我们后面的课程中会重点介绍,敬请期待。 那么利用这种特性,能够为我们解决什么问题呢?往下看: 计数器案例: ```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是全局变量,容易被修改。 ```js 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的作用域即可。 ```js function count() { var i = 1; return i++; } ``` 是这样吗?很显然不是,此时的i会在每次调用count的时候被初始化为1,所以达不到计数的效果。 ```js 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函数的私有成员,所以在函数外部无法访问,也就无法被修改,安全! 上面代码还可以做进一步的优化: ```js var ret = (function() { var i = 1; return function () { return i++; }; })(); ``` 在定义一个函数后,如果需要立即而且只执行一次的话,可以使用 **(函数对象)()**的语法结构,上面就是利用这种方式,立即执行函数将返回值(另一个函数)赋值给ret变量。 ## 函数使用的典型结构(IIFE立即执行函数) Immediately-Invoked Function Expression (*IIFE*):立即执行函数或者即时函数 这种函数就是上一节我们看到的效果,就是需要在函数定义之后立即执行一次,并且只能执行这一次,以后不能再调用,通常使用在初始化的阶段。 立即执行函数可以使用下面的语法来实现: ```js (function () { console.log("方式一"); })(); (function () { console.log("方式二"); }()); !function () { console.log("方式三"); }(); +function () { console.log("方式四"); }(); -function () { console.log("方式五"); }(); ~function () { console.log("方式六"); }(); ``` **题外话:函数,括号,语法错误** 有趣的是,如果你为一个函数指定了名称并且在立刻在其后边放置了括号,解析器也会抛出错误,但原因不同。虽然在表达式之后放置括号说明这是一个将被执行的函数,但在声明之后放置括号会与前面的语句分离,成为一个分组操作符(可以作为优先提升的方法)。 ```js // 现在这个函数声明的语法是正确的,但还是有报错 // 表达式后面的括号是非法的, 因为分组运算符必须包含表达式 function foo(){ /* code */ }(); // SyntaxError: Unexpected token ) // 如果你在括号内放置了表达式, 没有错误抛出... function foo(){ /* code */ }( 1 ); // 但是函数也不会执行,因为它与一个函数声明后面放一个完全无关的表达式是一样的 function foo(){ /* code */ } ( 1 ); ``` ## 函数使用的典型结构(惰性函数定义) 惰性函数:在函数中会进行一些分支判断或者初始化更新操作,然后将函数修改函数的指向,那么再次调用该函数的时候,执行的是修改之后指向的函数。 ```js function foo() { console.log("初始化操作"); foo = function () { console.log("真正的业务逻辑处理"); } } foo();//初始化操作 foo();//真正的业务逻辑处理 foo();//真正的业务逻辑处理 ``` 在foo函数中,我们可以先执行初始化相关的操作,然后将另外一个函数赋值给foo,所以再次调用foo的时候,执行的是对真实业务逻辑的处理操作。 惰性函数在JS中使用比较多,但是需要注意下面几点: 1. 函数对象中的属性在更新之后会丢失 ```js foo.des = "描述信息"; console.log(foo.des);//描述信息 foo();//初始化操作 foo();//真正的业务逻辑处理 foo();//真正的业务逻辑处理 console.log(foo.des);//undefined ``` 因为函数更新之后指向的是另外一个函数对象,在该函数对象中没有des这个属性。 2. 如果将惰性函数赋值给一个变量,通过这个变量调用该函数,此时无法执行到更新之后的函数 ```js function foo() { console.log("初始化操作"); foo = function () { console.log("真正的业务逻辑处理"); } } var f = foo; f();//初始化操作 f();//初始化操作 f();//初始化操作 ``` 因为调用f函数后,只是修改了foo指向另一个函数,而f仍然指向之前的函数,没有改变。 ![image.png](https://upload-images.jianshu.io/upload_images/11387429-c0fd2d495a4f88a6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## 作用域简单介绍 ## 函数和变量声明的提升 在JS中存在一个很重要的特性,函数和变量声明的提升,理解这一点对于理解我们编写的代码非常有帮助,那么声明是声明的提升呢?我们通过下面的代码来分析。 ```js console.log(a);//① var a = 123; console.log(a);//② console.log(f);//③ f();//④ function f() { console.log("函数声明提升"); } ``` ①处的代码如果按照我们以前的理解,代码从上而下执行,那么在执行这行代码的时候,a还没有被声明,所以直接访问一个没有被声明的变量,程序应该报错。 但是结果却大出所料,这里得到的结果是undefined。 ③处的结果也和我们最初的认识是不一样的,结果为f对应的函数对象。 造成这个结果是因为变量和函数的作用域提升的原因,什么意思呢? JS是解释性语言,JS引擎对代码的处理分为两步: ​ 预解析处理:在代码执行之前,对代码做全局扫描,对代码中出现的变量或者函数提前声明处理; ​ 解析之后我们的代码: ```js var a;//提前声明,但不初始化 console.log(a);//undefined a = 123; console.log(a);//123 //提前声明 function f() { console.log("函数声明提升"); } console.log(f);//函数对象 f();//函数声明提升 ``` ​ 具体的执行:自上而下的执行代码 如果有这样的认识的话,相信大家就能够顺利的分析出上面程序的正确结果了。 ## 变量提升和作用域的关系 下面给出两个练习帮助大家理解变量提升和作用域的关系。 **题1:** ```js 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:** ```js console.log(a); var a = 123; console.log(a); function f1() { console.log(a); var a = 456; console.log(a); } f1(); console.log(a); ``` 不废话,直接先对代码做预解析,然后再做分析。 ```js 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. 变量和变量同名,解析之后只存在一个当前变量的声明 ```js console.log(a); var a = 123; console.log(a); var a = 456; console.log(a); ``` 解析之后: ```js var a; console.log(a);//undefined a = 123; console.log(a);//123 a = 456; console.log(a);//456 ``` 2. 函数和函数同名,后面的声明将前面的覆盖 ```js f(); function f() { console.log("1"); } f(); function f() { console.log("2"); } f(); function f() { console.log("3"); } ``` 解析之后: ```js function f() { console.log("3"); } f();//3 f();//3 f();//3 ``` 3. 函数和变量同名,函数声明提升,忽略变量的声明 ```js console.log(a); var a = 123; console.log(a); function a() {} console.log(a); function a() {} console.log(a); ``` 解析之后: ```js function a() {} console.log(a);//函数a var a = 123;//将前面的函数覆盖,a的值变为123 console.log(a);//123 console.log(a);//123 console.log(a);//123 ``` 4. 如果是命名函数,则只将前面的变量声明提升,函数不动。 ```js console.log(fn1); function fn1() { } console.log(fn1); console.log(fn2); var fn2 = function () { } console.log(fn2); ``` 解析之后: ```js function fn1() { } var fn2; console.log(fn1);//fn1函数 console.log(fn1);//fn1函数 console.log(fn2);//undefined fn2 = function () { } console.log(fn2);//fn2函数 ``` ## 作用域链和访问规则 作用域链中存储了当前所在的作用域和能够访问到的作用域,如果我们要访问的变量不在当前作用域链中,那么程序会报错,反之从链上对应的作用域中获取到变量。 那么在作用域链中的成员访问规则是怎么样的呢? ```js 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个函数,他们层层嵌套,我们需要知道的是,每个函数中变量的作用域及其访问规则。 函数中定义的变量称为局部变量,它只属于当前函数的作用域及其嵌套函数的作用域中,外界无法访问。也就是一种由内而外的访问,反之则不行。 ![image.png](https://upload-images.jianshu.io/upload_images/11387429-fb3da42743182167.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## 闭包相关知识点复习 JS中每个函数都有自己的作用域,在当前函数中定义的变量,只能在该函数或该函数的嵌套函数中访问。 如: ```js function fun(){ var a = 123; console.log(a);//123 } console.log(a);//报错:a没有定义 ``` 在开发中,如果需要在当前函数作用域外访问函数的局部变量能实现吗?看下面的代码: ```js 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的值。 这种在函数中返回另一个函数的结构就称之为闭包,闭包在的主要作用就是打破函数的作用域的限制,能在需要的地方访问函数内部的私有成员。 ## 闭包访问和设置数据 上面我们通过闭包,能够访问到一个函数中的私有成员,那么如果需要访问其中的多个私有成员,应该如何实现呢? 这里大家应该能够想到,函数的返回值只能有一个,如果有多个值需要返回,我们完全可以使用数组来实现。 ```js 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 ``` 也或者是: ```js 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 ``` 上面两种方式都是使用数组实现访问多个私有成员的需求,在实际开发中不太好使,因为数组是使用所有来访问元素,如果数组中元素个数较多,那么这种方式就容易出错了。所有我们想到使用对象的形式来返回。 ```js 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); ``` 上面这种方式就好多了,将需要访问的函数定义到对象中,然后通过访问对象的属性来达到目的。 如此一来,我们也可以将访问变量的其他方法都定义在该对象中,如,为变量赋值的方法。 ```js 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 ``` 这是在以后开发中经常使用到的,大家可以多熟悉一下。 最后,使用前面学过的知识点来对上面的代码做一次优化。 ```js 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 ```

叩丁狼学员采访 叩丁狼学员采访
叩丁狼头条 叩丁狼头条
叩丁狼在线课程 叩丁狼在线课程