本篇为 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 函数运行时使用的this
值 -
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])
-
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; 代替之前所使用的 that
setTimeout(function () {
this.disabled = false;
}.bind(this), 2000); // bind 在定时器函数外面,this 指向 btn 对象
}
}
</script>
「三」严格模式
本节只是列举了部分常用的严格模式规范,更多可参考 MDN —— 严格模式
JavaScript 除了提供正常模式外,还提供了 严格模式(strict mode)。ES5 的严格模式是采用具有限制性 JavaScript 变体的一种方式,即在严格的条件下执行 JS 代码。
严格模式在 IE10 以上版本的浏览器中才会被支持,旧版本浏览器中会被忽略。
严格模式对正常的 JavaScript 语义做了一些更改:
- 消除了 JavaScript 语法的一些不合理、不严谨之处,减少了一些怪异行为
- 消除代码运行的一些不安全之处,保证代码运行的安全
- 提高编译器效率,增加运行速度
- 禁用了在 ECMAScript 的未来版本中可能会定义的一些语法,为未来新版本的 JavaScript 做好铺垫。比如一些保留字如:
class
、enum
、export
、import
、super
不能做变量名
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
对象。而在严格模式下,全局作用域中函数中的this
是undefined
- 以前构造函数时不加
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); // 张三
})
相当于
// 将函数传给 callback
var callback = function (a) {
console.log(a);
}
// 调用时,name 赋值给了 a
callback(name);
此处简单介绍一下,后文还会再讲解。
「五」闭包
1. 变量作用域
变量根据作用域的不同分为两种:全局变量和局部变量。
- 函数内部可以使用全局变量
- 函数外部不可以使用局部变量
- 当函数执行完毕,本作用域内的局部变量会被销毁
2. 闭包概念
闭包(closure)指有权访问另一个函数作用域中变量的 函数
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()
调用后被自动清除。
为什么会这样呢?原因就在于 f1
是 f2
的父函数,而 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 对象的 id
和 name
并不会影响 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 20
console.log(clone.id, clone.msg.age); // 1 18
注意:这里有一个小细节,要先判断是否为数组(Array),因为 Array 也属于 Object。如果先判断 Object,则 Array 也被当做 Object 进行处理了。