700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > JavaScript 面向对象编程(三) —— 函数进阶 / 严格模式 / 高阶函数 / 闭

JavaScript 面向对象编程(三) —— 函数进阶 / 严格模式 / 高阶函数 / 闭

时间:2021-02-23 18:26:41

相关推荐

JavaScript 面向对象编程(三) —— 函数进阶 / 严格模式 / 高阶函数 / 闭

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

系列笔记:

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

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

「一」函数的定义和调用

1. 函数的定义方式

函数声明方式 function 关键字(命名函数)函数表达式(匿名函数)new Function()new Function()

new Function ([arg1[, arg2[, ...argN]],] functionBody)

arg1, arg2, ... argN:被函数使用的参数的名称必须是合法命名的。参数名称是一个有效的 JavaScript 标识符的字符串,或者一个用逗号分隔的有效字符串的列表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, ...)

thisArg:可选的,指 function 函数运行时使用的thisarg1, arg2, ...:指定的参数列表

var o = {name: 'andy'}function fn() {console.log(this);};fn.call();// Windowfn.call(o);// Object

apply 方法

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

function.apply(thisArg, [argsArray])

thisArg:在 function 函数运行时使用的this值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。argsArray:可选,传递的值,必须包含在 数组 里面返回值就是函数的返回值,因为它就是调用函数

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, ...)

thisArg:调用绑定函数时作为this参数传递给目标函数的值arg1, arg2, ...:传递的其他参数返回由指定的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; 代替之前所使用的 thatsetTimeout(function () {this.disabled = false;}.bind(this), 2000);// bind 在定时器函数外面,this 指向 btn 对象}}</script>

「三」严格模式

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

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

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

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

消除了 JavaScript 语法的一些不合理、不严谨之处,减少了一些怪异行为消除代码运行的一些不安全之处,保证代码运行的安全提高编译器效率,增加运行速度禁用了在 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 的语法和行为,都做出了一些改变。

变量规定在正常模式中,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,变量都必须先用 var 命令声明,然后才能使用

严禁删除已经声明的变量

严格模式下 this 指向以前在全局作用域函数中的this指向window对象。而在严格模式下,全局作用域中函数中的thisundefined

以前构造函数时不加new也可以调用,可以当作普通函数调用,this指向全局对象。但严格模式下,如果构造函数不加new就调用,会报错

new实例化的构造函数指向创建的对象实例定时器的this还是指向Window事件、对象还是指向其调用者函数规范函数不能有重名的 参数函数必须声明在顶层。新版本的 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>

举个例子

利用高阶函数,从外部获取异步方法中数据的示例,如下代码:

function getData() {setTimeout(function () {var name = '张三';return name;}, 1000);}console.log(getData());// undefined

可以看出,如果直接打印getData()是无法得到想要的数据name的。这是因为setTimeout()是异步方法,其回调函数会在同步任务执行后才会执行,也就是说先执行console.log(getData());因此打印结果为undefined

可以利用高阶函数,让回调函数作为参数来解决此问题。如下代码:

function getData(callback) {setTimeout(function () {var name = '张三';callback(name);}, 1000);}getData(function (a) {console.log(a);// 张三})

相当于

// 将函数传给 callbackvar callback = function (a) {console.log(a);}// 调用时,name 赋值给了 a callback(name);

此处简单介绍一下,后文还会再讲解。

「五」闭包

1. 变量作用域

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

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

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(); // 999nAdd();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;// 起步价 13var 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));// 13console.log(taxi.extra(false)); // 13console.log(taxi.price(5));// 23console.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 20console.log(clone.id, clone.msg.age); // 1 20

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

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

Object.assign(target, ...sources)

target:目标对象,拷贝给谁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 20console.log(clone.id, clone.msg.age); // 1 18

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

JavaScript 面向对象编程(三) —— 函数进阶 / 严格模式 / 高阶函数 / 闭包 / 浅拷贝和深拷贝

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。