Java教程

JavaScript 面向对象编程(三) —— 函数进阶

本文主要是介绍JavaScript 面向对象编程(三) —— 函数进阶,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

本篇为 JavaScript 进阶 ES6 系列笔记第三篇,将陆续更新后续内容。参考:JavaScript 进阶面向对象 ES6 ;ECMAScript 6 入门

系列笔记:

JavaScript 面向对象编程(一) —— 面向对象基础

JavaScript 面向对象编程(二) —— 构造函数 / 原型 / 继承 / ES5 新增方法

 

「一」函数的定义和调用


1. 函数的定义方式

  1. 函数声明方式 function 关键字(命名函数)
  2. 函数表达式(匿名函数)
  3. new Function()
  • new Function()
new Function ([arg1[, arg2[, ...argN]],] functionBody)
  1. arg1, arg2, ... argN:被函数使用的参数的名称必须是合法命名的。参数名称是一个有效的 JavaScript 标识符的字符串,或者一个用逗号分隔的有效字符串的列表
  2. functionBody:一个含有包括函数定义的 JavaScript 语句的字符串

在这里插入图片描述

这种方式执行效率低,不方便书写,较少使用。但是,通过此方式可以知道,所有函数都是 Function 的实例对象,即函数也属于对象。
在这里插入图片描述

2. 函数调用方式


此前学习了六种函数,它们分别是:普通函数、对象的方法、构造函数、绑定事件函数、定时器函数、立即执行函数。具体调用方法如下:

// 1. 普通函数
function fn() {
    console.log('普通函数');
}
fn();       // fn.call()

// 2. 对象的方法
var o = {
    sayHi: function () {
        console.log('对象方法');
    }
}
o.sayHi();

// 3. 构造函数
function Star() { };
new Star();                 

// 4. 绑定事件函数
btn.onclick = function () { };       // 点击调用

// 5. 定时器函数
setInterval(function () { }, 1000);   // 每隔 1 秒调用

// 6. 立即执行函数
(function () { })();        // 自动调用

 

「二」函数内部 this 指向


this 的指向是当我们调用函数的时候才被确定的,不同调用方式决定了 this 的不同指向。一般情况下,this 指向函数调用者。

在这里插入图片描述

改变函数内部 this 指向


JavaScript 为我们提供了一些函数方法来帮我们更优雅地处理内部 this 的指向问题,常用的有 bind()call()apply() 三种方法。

  • call 方法

call() 方法调用一个对象。可以简单理解为调用函数的方式,但是它可以改变函数的 this 指向。

function.call(thisArg, arg1, arg2, ...)
  1. thisArg:可选的,指 function 函数运行时使用的 this
  2. arg1, arg2, ...:指定的参数列表
        var o = {
            name: 'andy'
        }

        function fn() {
            console.log(this);
        };

        fn.call();      // Window
        fn.call(o);     // Object
  • apply 方法

apply() 方法调用一个具有给定this值的函数,以及以一个数组(或类数组对象)的形式提供的参数。

function.apply(thisArg, [argsArray])
  1. thisArg:在 function 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。
  2. argsArray:可选,传递的值,必须包含在 数组 里面
  3. 返回值就是函数的返回值,因为它就是调用函数
        var o = {
            name: 'andy'
        }
        function fn(arr) {
            console.log(this);
            console.log(arr);
        }
        fn.apply(o, ['pink']);  // Object pink

        // apply 应用 
        var arr = [1, 3, 2, 6, 5];
        var max = Math.max.apply(Math, arr);
        var min = Math.min.apply(Math, arr);
        console.log(max, min);   // 6 1
  • bind 方法

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

function.bind(thisArg, arg1, arg2, ...)
  1. thisArg:调用绑定函数时作为 this 参数传递给目标函数的值
  2. arg1, arg2, ...:传递的其他参数
  3. 返回由指定的 this 值和初始化参数改造的 原函数拷贝
        var o = {
            name: 'andy'
        };

        function fn() {
            console.log(this);
        }
        var f = fn.bind(o);     // 不会调用原函数
        f();    // Object

实际开发价值:如果有些函数我们不需要立即调用,但是又想改变这个函数内部的 this 指向,此时使用 bind() 是最方便的。

  • 案例:3 秒后恢复点击

请添加图片描述

<button>点击</button>
<button>点击</button>
<button>点击</button>
<script>
    var btns = document.querySelectorAll('button');
    for (var i = 0; i < btns.length; i++) {
        btns[i].onclick = function () {
            this.disabled = true;
            // var that = this;  代替之前所使用的 that
            setTimeout(function () {
                this.disabled = false;
            }.bind(this), 2000);	// bind 在定时器函数外面,this 指向 btn 对象
        }
    }
</script>

 

「三」严格模式


本节只是列举了部分常用的严格模式规范,更多可参考 MDN —— 严格模式

JavaScript 除了提供正常模式外,还提供了 严格模式(strict mode)。ES5 的严格模式是采用具有限制性 JavaScript 变体的一种方式,即在严格的条件下执行 JS 代码。

严格模式在 IE10 以上版本的浏览器中才会被支持,旧版本浏览器中会被忽略。

严格模式对正常的 JavaScript 语义做了一些更改:

  1. 消除了 JavaScript 语法的一些不合理、不严谨之处,减少了一些怪异行为
  2. 消除代码运行的一些不安全之处,保证代码运行的安全
  3. 提高编译器效率,增加运行速度
  4. 禁用了在 ECMAScript 的未来版本中可能会定义的一些语法,为未来新版本的 JavaScript 做好铺垫。比如一些保留字如:classenumexportimportsuper 不能做变量名

1. 开启严格模式


严格模式可以应用到整个脚本或个别函数中。因此在使用时,我们可以将严格模式分为 为脚本开启严格模式 为函数开启严格模式 两种情况。

  • 为脚本开启严格模式

为整个脚本文件开启严格模式,需要在所有语句之前放一个特定语句 "use strict" (或 'use strict')

    <!-- 为整个脚本(script 标签)开启严格模式 -->
    <script>
        'use strict';   // 以下 JS 代码按严格模式来执行
    </script>

有的 script 脚本是严格模式,有的 script 脚本是正常模式,这样不利于文件合并,所以可以将整个脚本文件放在一个立即执行的匿名函数之中。这样独立创建一个作用域而不影响其他 script 脚本文件。

    <script>
        // 开启独立的作用域空间,防止变量污染
        (function () {
            'use strict';
        })();
    </script>
  • 为函数开启严格模式

要给某个函数开启严格模式,需要把 "use strict" (或 'use strict')声明放在函数体所有语句之前。

    <script>
        function fn() {
            'use strict';
            // 下面代码按严格模式执行
        }

        function fun() {
            // 仍按照普通模式执行
        }
    </script>

 

2. 严格模式的规范


严格模式对 JavaScript 的语法和行为,都做出了一些改变。

  • 变量规定
  1. 在正常模式中,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,变量都必须先用 var 命令声明,然后才能使用
    在这里插入图片描述
  2. 严禁删除已经声明的变量
    在这里插入图片描述
  • 严格模式下 this 指向
  1. 以前在全局作用域函数中的 this 指向 window 对象。而在严格模式下,全局作用域中函数中的 thisundefined
    在这里插入图片描述
  2. 以前构造函数时不加 new 也可以调用,可以当作普通函数调用,this 指向全局对象。但严格模式下,如果构造函数不加 new 就调用,会报错
    在这里插入图片描述
  3. new 实例化的构造函数指向创建的对象实例
  4. 定时器的 this 还是指向 Window
  5. 事件、对象还是指向其调用者
  • 函数规范
  1. 函数不能有重名的 参数
  2. 函数必须声明在顶层。新版本的 JavaScript 会引入 “块级作用域” (ES6 中已经引入)。为了与新版本接轨,不允许在非函数的代码块内声明函数
	"use strict";
	if (true) {
	  function f() { } // !!! 语法错误
	  f();
	}
	
	for (var i = 0; i < 5; i++) {
	  function f2() { } // !!! 语法错误
	  f2();
	}
	
	function baz() { // 合法
	  function eit() { } // 同样合法
	}

 

「四」高阶函数


高阶函数是对其他函数进行操作的函数,它 接收函数作为参数将函数作为返回值输出

下面是 fn 为高阶函数的两种情况:

    <script>
        function fn(callback) {
            callback && callback();
        }
        fn(function () { });
    </script>
    <script>
        function fn() {
            return function () { }
        }
        fn();
    </script>

 

「五」闭包


1. 变量作用域

变量根据作用域的不同分为两种:全局变量和局部变量。

  1. 函数内部可以使用全局变量
  2. 函数外部不可以使用局部变量
  3. 当函数执行完毕,本作用域内的局部变量会被销毁
     

2. 闭包概念

闭包(closure)指有权访问另一个函数作用域中变量的 函数 。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成 " 定义在一个函数内部的函数 " 。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

   function f1() {
       var n = 999;
       function f2() {
           console.log(n);
       }
       return f2;
   }

   var result = f1();
   result(); // 999

上述代码中的 f2() 函数,就是闭包。它是典型的高阶函数,实现了从外部读取局部变量。

 

3. 闭包作用

参考 学习 Javascript 闭包(Closure)

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

  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() 调用后被自动清除。

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

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

4. 闭包应用

  • 案例:动态打印 li 标签索引号
    <ul class="nav">
        <li>海绵宝宝</li>
        <li>派大星</li>
        <li>章鱼哥</li>
        <li>蟹老板</li>
    </ul>
    <script>
        var lis = document.querySelector('.nav').querySelectorAll('li');
        // 1. 动态添加属性方式获得索引
        for (var i = 0; i < lis.length; i++) {
            lis[i].index = i;
            lis[i].onclick = function () {
                console.log(this.index);
            }
        }
        // 2. 利用闭包的方式得到索引
        for (var i = 0; i < lis.length; i++) {
            (function (i) {
                lis[i].onclick = function () {
                    console.log(i);
                }
            })(i);
        }
    </script>

在这里插入图片描述

  • 案例:3 秒后打印各元素内容
    <ul class="nav">
        <li>海绵宝宝</li>
        <li>派大星</li>
        <li>章鱼哥</li>
        <li>蟹老板</li>
    </ul>
    <script>
        var lis = document.querySelector('.nav').querySelectorAll('li');
        for (var i = 0; i < lis.length; i++) {
            (function (i) {
                setTimeout(function () {
                    console.log(lis[i].innerHTML);
                }, 3000)
            })(i);
        }
    </script>

上述两例中绑定点击事件、定时器都属于异步任务,异步任务只有当被触发时才会被推入任务队列依次执行。因此,利用了立即执行函数将对应索引传入。

  • 案例:计程车价格
    var taxi = (function () {
        var start = 13;     // 起步价 13
        var total = 0;      // 总价
        return {
            price: function (n) {
                total = total < 3 ? start : (start + (n - 3) * 5);
                return total;
            },
            extra: function (flag) {
                total = flag ? total + 10 : total;
                return total;
            }
        }
    })();

    console.log(taxi.price(1));     // 13
    console.log(taxi.extra(false)); // 13
    console.log(taxi.price(5));     // 23
    console.log(taxi.extra(true));  // 33
  • 思考题

下面看两道思考题来理解闭包的运行机制。

代码一

    var name = "The Window";
    var object = {
        name: "My Object",
        getNameFunc: function () {
            return function () {
                return this.name;   // this 指向 Window
            };
        }
    };

    console.log(object.getNameFunc()());  // The Window

代码二

    var name = "The Window";
    var object = {
        name: "My Object",
        getNameFunc: function () {
            var that = this;
            return function () {
                return that.name;   // this 指向 object
            };
        }
    };
    console.log(object.getNameFunc()());    // My Object

5. 闭包缺陷

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

  • 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
     

「六」递归


如果一个函数在内部可以调用本身,那么这个函数就是 递归函数

如下代码,用递归求 n 的阶乘:

    function fn(n) {
        if (n == 1)
            return 1;
        return n * fn(n - 1);
    }
  • 案例:利用递归遍历数据
    var data = [{
        id: 1,
        name: '家电',
        goods: [{
            id: 11,
            gname: '冰箱',
            goods: [{
                id: 111,
                gname: 'Hair'
            }, {
                id: 112,
                gname: 'Media'
            }]
        }, {
            id: 12,
            gname: '洗衣机'
        }]
    }, {
        id: 2,
        name: '服饰'
    }]

    // forEach 遍历
    function getData(json, id) {
        var o = {};
        json.forEach(function (item) {
            // 遍历外层
            if (item.id == id) {
                // console.log(item);
                o = item;
            }
            // 遍历外层 
            else if (item.goods && item.goods.length > 0) {
                o = getData(item.goods, id);
            }
        });
        return o;
    }
    console.log(getData(data, 1));      // {id: 1, name: '家电', goods: Array(2)}
    console.log(getData(data, 11));     // {id: 11, gname: '冰箱', goods: Array(2)}
    console.log(getData(data, 111));    // {id: 111, gname: 'Hair'}

 

「七」浅拷贝和深拷贝


1. 直接赋值

谈到拷贝,其实就是将对象复制一份给另一个对象,如下所示代码为将一个对象直接赋值给另一个对象:

    var obj = {
        id: 1,
        name: 'andy'
    };
    var clone = obj;	// 直接赋值,将对所有的对象属性方法进行浅拷贝
    obj.id = 2;

    console.log(clone.id);  // 2

可以发现,尽管只将 obj 中的 id 属性进行修改了,但是 clone 中的 id 属性也发生了变化。这是因为,当创建 obj 对象时,它在堆内存中开辟了一块空间存储对象的内容。而当 clone 直接赋值为 obj 时,clone 并不会再重新开辟一块堆内存,而是将这块内存空间存储的对象的地址给 clone

在这里插入图片描述

2. 浅拷贝


与直接赋值的方式不同,浅拷贝是 只拷贝一层,更深层次对象级别的只拷贝引用

    var obj = {
        id: 1,
        name: 'andy',
        msg: {
            age: 18
        }
    };
    var clone = {};
    for (var k in obj) {
        // k 是属性名   obj[k] 属性值
        clone[k] = obj[k];
    }
    obj.id = 2;
    obj.msg.age = 20;
    console.log(obj.id, obj.msg.age);       // 2  20
    console.log(clone.id, clone.msg.age);   // 1  20

注意:与直接赋值 var clone = obj; 不同,此处进行浅拷贝的内容是更深层次的对象 msg: { age: 18 },只拷贝其引用。而单独修改 obj 对象的 idname 并不会影响 clone 对象中相应属性的值。内存中关系如下图所示
在这里插入图片描述

实现浅拷贝还可以使用 ES6 新增浅拷贝方法

Object.assign(target, ...sources)
  1. target:目标对象,拷贝给谁
  2. sources:源对象,要拷贝的对象

示例

    Object.assign(clone, obj);

等价于普通写法

    for (var k in obj) {
        // k 是属性名   obj[k] 属性值
        clone[k] = obj[k];
    }

3. 深拷贝


深拷贝就不会像浅拷贝那样只拷贝一层,而是将每一级别的数据都进行拷贝,要真正的做到全部内容都放在自己新开辟的内存里,可以 利用递归思想实现深拷贝

      var obj = {
          id: 1,
          name: 'andy',
          msg: {
              age: 18
          },
          color: ['blue', 'orange']
      };
      var clone = {}
      function deepCopy(newobj, oldobj) {
          for (var k in oldobj) {
              var item = oldobj[k];
              // 分别判断数组、对象、简单数据类型
              if (item instanceof Array) {
                  newobj[k] = [];
                  deepCopy(newobj[k], item)
              } else if (item instanceof Object) {
                  newobj[k] = {};
                  deepCopy(newobj[k], item);
              } else {
                  newobj[k] = item;
              }
          }
      }
      deepCopy(clone, obj);

      obj.id = 2;
      obj.msg.age = 20;
      console.log(obj.id, obj.msg.age);       // 2  20
      console.log(clone.id, clone.msg.age);   // 1  18

注意:这里有一个小细节,要先判断是否为数组(Array),因为 Array 也属于 Object。如果先判断 Object,则 Array 也被当做 Object 进行处理了。
在这里插入图片描述

这篇关于JavaScript 面向对象编程(三) —— 函数进阶的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!