ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

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

2021-10-17 20:59:56  阅读:132  来源: 互联网

标签:function console 进阶 JavaScript log 面向对象编程 var id 函数


本篇为 JavaScript 进阶 ES6 系列笔记第三篇,将陆续更新后续内容。参考:JavaScript 进阶面向对象 ES6ECMAScript 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 进行处理了。
在这里插入图片描述

标签:function,console,进阶,JavaScript,log,面向对象编程,var,id,函数
来源: https://blog.csdn.net/qq_46331050/article/details/120762998

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有