第六章 语法深入
一、回调函数
在js里参数可以直接传方法。
重要的事情说三遍,在java里不行,在java里不,行在java里不行。
//这个函数负责从后天获取数据
var request = function(callback){
console.log("从服务器获取数据!")
var data = 123;
callback(data);
}
//回调通常是我们写的,等参数被请求完成后执行的函数
var callb = function(data){
console.log("我从服务器获取了数据:"+data);
document.getElementById("div1").innerHTML = data;
}
request(callb);
结果:
VM500:2 从服务器获取数据!
VM679:2 我从服务器获取了数据:123
//使用之中回调函数,屏蔽了难以实现的延时操作,我们只需要关系几秒之后发生的事情(传入的回调方法)就行了
setTimeout(function(){
console.log(123)
},1000)
//模仿setTimeOut写一下
var mySetTimeOut = function(callback,delay){
console.log("已经过了"+delay+"毫秒");
callback();
}
//写一个回调假装实现
mySetTimeOut(function(){
console.log(123)
},2000)
结果:
VM561:2 已经过了2000毫秒
VM636:2 123
二、方法默认传入的形参
1、arguments
方法会将调用时传入的所有参数封装成一个类数组。
js对传参要求的非常灵活,基本上就是想怎么传就怎么传。
所以最重要的一点就是,怎要合适的利用参数。
命名是个数字,就不要当成对象用,命名是个字符串就不要当成数组用。
function test(){
for( let i = 0 ; i < arguments.length ; i++ ){
console.log(arguments[i])
}
}
test(1,'2342',34,456,678,789,null);
VM1546:3 1
VM1546:3 2342
VM1546:3 34
VM1546:3 456
VM1546:3 678
VM1546:3 789
VM1546:3 null
function test1(){
for( var i = 0 ; i < arguments.length ; i++ ){
document.getElementById(arguments[i]).style = 'background:blue'
}
}
test1()
test1('div1','div2')
2、this
this总是指向调用这个方法的实例对象。
在浏览器中,直接定义一个方法,其实是定义在了window这个对象之中,所以直接调用方法其实是window.方法名(),因为在window环境下,所以window通常不用写。所有如果直接调用这个方法,this会指向window。
function test(){
console.log(this)
}
test()
和
window.test()
一样
结果:
VM1620:2 Window {parent: Window, opener: null, top: Window, length: 0, frames: Window, …}
function User(name){
this.name = name;
this.print = function(){
console.log(this)
}
}
let user = new User("李兴");
user.name
"李兴"
//使用user调用print this就是这个user
user.print()
VM1786:4 User {name: "李兴", print: ƒ}
let dog = {
name: 'teddy',
say: function (a,b) {
var that = this;
console.log(a,b)
setTimeout(function(){
console.log('my name is ' + that.name)
}, 1000);
}
}
dog.say();
// 这里say方法调用时的this指向调用say的dog对象,
// 而setTimeout方法调用时是由window对象负责调用,
// 所以setTimeoue的this指向window。
// 如果要在setTimeout内使用dog对象需要在外边进行保存
this指向的改变
使用call、apply、bind可以改变this的指向
1、第一个参数都是新的this对象
2、从第二个参数开始,需要传递say方法的实参,
3、call是以多个参数的方式传递,而apply是以数组形式传递
4、bind不能直接执行方法,而是返回一个方法,需要另行执行
dog.say.call({name: '刘奇'},12,23)
dog.say.apply({name: '刘奇'},[23,45])
var fn = dog.say.bind({name: '刘奇'})
fn()
三、作用域
全局作用域只有一个,每个函数又都有作用域(环境)。
- 编译器运行时会将变量定义在所在作用域
- 使用变量时会从当前作用域开始向上查找变量
- 作用域就像攀亲亲一样,晚辈总是可以向上辈要些东西
1、作用域链
作用域链只向上查找,找到全局window即终止,应该尽量不要在全局作用域中添加变量。
函数被执行后其环境变量将从内存中删除。下面函数在每次执行后将删除函数内部的total变量。
function count() {
let total = 0;
}
count();
函数每次调用都会创建一个新作用域
let site = 'itnanls';
function a() {
let hd = 'zn.com';
function b() {
let cms = 'itnanls.cn';
console.log(hd);
console.log(site);
}
b();
}
a();
如果子函数被使用时父级环境将被保留
function hd() {
let n = 1;
return function() {
let b = 1;
return function() {
console.log(++n);
console.log(++b);
};
};
}
let a = hd()();
a(); //2,2
a(); //3,3
2、块作用域
使用 let/const
可以将变量声明在块作用域中(放在新的环境中,而不是全局中)
{
let a = 9;
}
console.log(a); //ReferenceError: a is not defined
if (true) {
var i = 1;
}
console.log(i);//1
也可以通过下面的定时器函数来体验
for (let i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 500);
}
在 for
循环中使用let/const
会在每一次迭代中重新生成不同的变量
let arr = [];
for (let i = 0; i < 10; i++) {
arr.push((() => i));
}
console.log(arr[3]()); //3 如果使用var声明将是10
在没有let/const
的历史中使用以下方式产生作用域
//自行构建闭包
var arr = [];
for (var i = 0; i < 10; i++) {
(function (a) {
arr.push(()=>a);
})(i);
}
console.log(arr[3]()); //3
四、闭包使用
闭包指子函数可以访问外部作用域变量的函数特性,即使在子函数作用域外也可以访问。如果没有闭包那么在处理事件绑定,异步请求时都会变得困难。
- JS中的所有函数都是闭包
- 闭包一般在子函数本身作用域以外执行,即延伸作用域
一、基本示例
前面在讲作用域时已经在使用闭包特性了,下面再次重温一下闭包。
function hd() {
let name = '欣知';
return function () {
return name;
}
}
let xzcms = hd();
console.log(xzcms()); //欣知
二、使用闭包做计数器
计时器中使用闭包来获取独有变量
var adder = (function(start) {
return function() {
return start++;
}
})(10);
console.log(adder());
console.log(adder());
console.log(adder());
adder = null;
三、使用闭包做缓存
var plus = (function () {
var cache = {};
return function () {
var key = [].join.call(arguments);
if (key in cache) {
console.log('走了缓存')
return cache[key]
}
console.log('又计算了一次')
var sum = 0;
for (var i = 0; i < arguments.length; i++) {
sum += arguments[i]
}
cache[key] = sum;
return sum;
}
})();
plus(1,2,3,4);
plus(1,2,3,4);
plus(1,2,3,4);
四、闭包问题
内存泄漏
闭包特性中上级作用域会为函数保存数据,从而造成的如下所示的内存泄漏问题
<body>
<div desc="zn">在线学习</div>
<div desc="xzcms">开源产品</div>
</body>
<script>
let divs = document.querySelectorAll("div");
divs.forEach(function(item) {
item.addEventListener("click", function() {
console.log(item.getAttribute("desc"));
});
});
</script>
下面通过清除不需要的数据解决内存泄漏问题
let divs = document.querySelectorAll("div");
divs.forEach(function(item) {
let desc = item.getAttribute("desc");
item.addEventListener("click", function() {
console.log(desc);
});
item = null;
});
五、迭代器与生成器
1. 迭代器
1.1 什么是迭代器
- 宏观上来讲,迭代器的定义我们可以看出来,迭代器是 帮助我们对某个数据结构进行遍历的对象,无需关心内部实现,只需要能使用其接口和特性即可。
- 微观上来讲(JS 角度),迭代器是一个具体的对象,这个对象需要符合 迭代器协议(iterator protocol)。
1.2 迭代器协议
- 迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方式
- 在 JS 中这个标准就是一个特定的
next
方法;
1.3 next 函数(方法)
next
是一个无参或者一个参数的函数,返回一个具有两个属性(done
和 value
)的对象。
next: function() {
return {
done: boolean,
value: any
}
}
返回的对象属性:
done
:true
表示迭代结束,flase
未结束。value
: 本次迭代的值。
1.4 举例:封装一个迭代器函数
const colors = ['red', 'pink', 'green']
const nums = [100, 200, 300]
const iteratorFunc = arr => {
let index = 0
return {
next() {
if (index < arr.length) {
return { done: false, value: arr[index++] }
}
return { done: true, value: undefined }
}
}
}
const colorsIterator = iteratorFunc(colors)
const numsIterator = iteratorFunc(nums)
console.log(colorsIterator.next())
console.log(colorsIterator.next())
console.log(colorsIterator.next())
console.log(colorsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
1.5 可迭代对象
- 当一个对象实现了 iterable protocol 协议 时,它就是一个可迭代对象。
- 这个对象的要求是必须实现
@@iterator
方法,在代码中我们使用Symbol.iterator
访问该属性
如何定义一个可迭代对象
- 给该对象添加
[Symbol.iterator](){}
函数(方法)。
可迭代对象有什么用
可迭代对象为可迭代特性服务,也就是说,一个对象实现了可迭代性,那么他就具有一些列可迭代对象所具有的一切特性。
JavaScript中语法:
for ... of
、展开语法(spread syntax)、yield*
(后面讲)、解构赋值(Destructuring_assignment);创建一些对象时:
new Map([Iterable])
、new WeakMap([iterable])
、new Set([iterable])
、new WeakSet([iterable])
;一些方法的调用:
Promise.all(iterable)
、Promise.race(iterable)
、Array.from(iterable)
;
一个对象具有可迭代特性的条件
- 底层实现了可迭代方法,JS 或者就是
[Symbol.iterator](){}
,例如数组、字符串、类数组、Map
、Set
、NodeList
等。检测是否是可迭代对象:
Array.prototype[Symbol.iterator];
String.prototype[Symbol.iterator];
Map.prototype[Symbol.iterator];
Set.prototype[Symbol.iterator];
[!TIP] 一般对象没有实现
[Symbol.iterator]()
,但 ES9 底层做了处理,让对象也支持解构赋值和扩展运算符。
创建一个可迭代对象
const iterableObj = {
colors: ['red', 'green', 'orange'],
[Symbol.iterator]() {
let index = 0
return {
next: () => {
if (index < this.colors.length) {
return { done: false, value: this.colors[index++] }
}
return { done: true, value: undefined }
}
}
}
}
// 1、let ... of ...
for (let color of iterableObj) {
console.log(color)
}
// 2、spread
console.log([...iterableObj])
// 3、distructing assignment
const [red] = iterableObj
console.log(red)
// 4、new Set
console.log(new Set(iterableObj))
// 5、Array.from
console.log(Array.from(iterableObj))
// 6、Promise.all()
Promise.all(iterableObj).then(res => {
console.log(res)
})
2. 生成器
2.1 什么是生成器
- 生成器是一个函数,能让我们更加方便的控制函数的暂停、继续执行。
- 生成器是一种特殊的迭代器,一般可以替代迭代器,简化代码
- 生成器的返回值还是一个生成器(Generator)
2.2 生成器替代迭代器的三种方法
// 生成器是一种特殊的迭代器,所以可以代替迭代器
function* createIterableArray(arr) {
// 1、写法1
// for (const e of arr) {
// yield e
// }
// 2、写法2
// yield arr[0]
// yield arr[1]
// yield arr[2]
// 3、写法3
yield* arr
}
const names = ['peter', 'jack', 'julia']
const namesIterator = createIterableArray(names)
for (let item of namesIterator) {
console.log(item)
}
2.3 async/await 与 生成器
在 async/await
出现之前,回调地狱问题的解决是靠生成器解决的。 async/await
的原理就是生成器。
模拟请求:
// request.js
function requestData(url) {
// 异步请求的代码会被放入到executor中
return new Promise((resolve, reject) => {
// 模拟网络请求
setTimeout(() => {
// 拿到请求的结果
resolve(url)
}, 2000);
})
}
需求:
- url: why -> res: why
- url: res + "aaa" -> res: whyaaa
- url: res + "bbb" => res: whyaaabbb
- 第一种方案: 多次回调
// 回调地狱
requestData("why").then(res => {
requestData(res + "aaa").then(res => {
requestData(res + "bbb").then(res => {
console.log(res)
})
})
})
- 第二种方案: Promise中then的返回值来解决
requestData("why").then(res => {
return requestData(res + "aaa")
}).then(res => {
return requestData(res + "bbb")
}).then(res => {
console.log(res)
})
- 第三种方案: Promise + generator实现
function* getData() {
const res1 = yield requestData("why")
const res2 = yield requestData(res1 + "aaa")
const res3 = yield requestData(res2 + "bbb")
const res4 = yield requestData(res3 + "ccc")
console.log(res4)
}
// 1> 手动执行生成器函数
// const generator = getData()
// generator.next().value.then(res => {
// generator.next(res).value.then(res => {
// generator.next(res).value.then(res => {
// generator.next(res)
// })
// })
// })
// 2> 自己封装了一个自动执行的函数
function execGenerator(genFn) {
const generator = genFn()
function exec(res) {
const result = generator.next(res)
if (result.done) {
return result.value
}
result.value.then(res => {
exec(res)
})
}
exec()
}
execGenerator(getData)
- 第四种方案: async/await
async function getData() {
const res1 = await requestData("why")
const res2 = await requestData(res1 + "aaa")
const res3 = await requestData(res2 + "bbb")
const res4 = await requestData(res3 + "ccc")
console.log(res4)
}
getData()
3. 迭代器与生成器问题
实现以下效果:
>>> [...5]
[1, 2, 3, 4, 5]
答案解析:http://idk-js.mphy.top/JS/JavaScript.html#_11-10