本篇为 JavaScript 进阶 ES6 系列笔记第三篇,将陆续更新后续内容。参考:JavaScript 进阶面向对象 ES6 ;ECMAScript 6 入门
系列笔记:
JavaScript 面向对象编程(一) —— 面向对象基础
JavaScript 面向对象编程(二) —— 构造函数 / 原型 / 继承 / ES5 新增方法
new Function ([arg1[, arg2[, ...argN]],] functionBody)
arg1, arg2, ... argN
:被函数使用的参数的名称必须是合法命名的。参数名称是一个有效的 JavaScript 标识符的字符串,或者一个用逗号分隔的有效字符串的列表functionBody
:一个含有包括函数定义的 JavaScript 语句的字符串这种方式执行效率低,不方便书写,较少使用。但是,通过此方式可以知道,所有函数都是 Function
的实例对象,即函数也属于对象。
此前学习了六种函数,它们分别是:普通函数、对象的方法、构造函数、绑定事件函数、定时器函数、立即执行函数。具体调用方法如下:
// 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
指向函数调用者。
JavaScript 为我们提供了一些函数方法来帮我们更优雅地处理内部 this
的指向问题,常用的有 bind()
、call()
、apply()
三种方法。
call()
方法调用一个对象。可以简单理解为调用函数的方式,但是它可以改变函数的 this
指向。
function.call(thisArg, arg1, arg2, ...)
thisArg
:可选的,指 function 函数运行时使用的 this
值arg1, arg2, ...
:指定的参数列表var o = { name: 'andy' } function fn() { console.log(this); }; fn.call(); // Window fn.call(o); // Object
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()
被调用时,这个新函数的 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()
是最方便的。
<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 语义做了一些更改:
class
、enum
、export
、import
、super
不能做变量名严格模式可以应用到整个脚本或个别函数中。因此在使用时,我们可以将严格模式分为 为脚本开启严格模式 和 为函数开启严格模式 两种情况。
为整个脚本文件开启严格模式,需要在所有语句之前放一个特定语句 "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>
严格模式对 JavaScript 的语法和行为,都做出了一些改变。
this
指向 window
对象。而在严格模式下,全局作用域中函数中的 this
是 undefined
new
也可以调用,可以当作普通函数调用,this
指向全局对象。但严格模式下,如果构造函数不加 new
就调用,会报错new
实例化的构造函数指向创建的对象实例this
还是指向 Window
"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>
变量根据作用域的不同分为两种:全局变量和局部变量。
闭包(closure)指有权访问另一个函数作用域中变量的 函数 。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成 " 定义在一个函数内部的函数 " 。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
function f1() { var n = 999; function f2() { console.log(n); } return f2; } var result = f1(); result(); // 999
上述代码中的 f2()
函数,就是闭包。它是典型的高阶函数,实现了从外部读取局部变量。
参考 学习 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()
调用后被自动清除。
为什么会这样呢?原因就在于 f1
是 f2
的父函数,而 f2
被赋给了一个全局变量,这导致 f2
始终在内存中,而 f2
的存在依赖于 f1
,因此 f1
也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
这段代码中另一个值得注意的地方,就是 nAdd = function() { n += 1 }
这一行,首先在 nAdd
前面没有使用 var
关键字,因此 nAdd
是一个全局变量,而不是局部变量。其次,nAdd
的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以 nAdd
相当于是一个 setter
,可以在函数外部对函数内部的局部变量进行操作。
<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>
<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
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除(如将变量赋值为 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'}
谈到拷贝,其实就是将对象复制一份给另一个对象,如下所示代码为将一个对象直接赋值给另一个对象:
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
。
与直接赋值的方式不同,浅拷贝是 只拷贝一层,更深层次对象级别的只拷贝引用 。
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 对象的 id
和 name
并不会影响 clone 对象中相应属性的值。内存中关系如下图所示
实现浅拷贝还可以使用 ES6 新增浅拷贝方法
Object.assign(target, ...sources)
示例
Object.assign(clone, obj);
等价于普通写法
for (var k in obj) { // k 是属性名 obj[k] 属性值 clone[k] = obj[k]; }
深拷贝就不会像浅拷贝那样只拷贝一层,而是将每一级别的数据都进行拷贝,要真正的做到全部内容都放在自己新开辟的内存里,可以 利用递归思想实现深拷贝 。
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 进行处理了。