重点内容提要:JS 函数的执行时机
1)为什么如下代码会打印 6 个 6
1
2
3
4
5
6
|
let i = 0
for(i = 0; i<6; i++){
setTimeout(()=>{
console.log(i)
},0)
}
|
我认为本篇正文涉及到的知识(作用域、闭包的定义、 调用时机、调用栈等)不足以解释
任何试图解释这个现象的尝试目前都是徒劳
所以我直接搜了并总结了各类答案:setTimeout函数在队列任务执行完后执行console.log(i)
,在此之前,for
的判断表达式与自增语句就早已全部走完,i
值为6
1
2
3
4
5
6
7
8
9
|
let i = 0
for(i = 0; i < 6;console.log('自增语句前的i:' + i) || i++ && console.log('自增语句后的i:' + i)){
console.log('循环体中setTimeOut()前的i:' + i) // 0、1、2、3、4、5
setTimeout(()=>{
console.log('setTimeOut延时打印出的i:' + i) // 6个6
},0);
console.log('循环体中setTimeOut()后的i:' + i) // 0、1、2、3、4、5
}
console.log('循环外打印出的i:' + i) // 6个6
|
可能至少但不限于先要了解的知识,如下
提供了几个思考的维度
以后慢慢填坑,还瞥见了Promise、V8
先交次作业再说
2)让上面代码打印 0、1、2、3、4、5
的方法
利用 for let
配合
- 最简洁的办法,最省脑子,最易上手,代码量最少,工作效率
1
2
3
4
5
|
for(let i = 0; i<6; i++){
setTimeout(()=>{
console.log(i)
},0)
}
|
利用 const 声明常量
1
2
3
4
5
6
7
|
let i
for(i = 0; i<6; i++){
const x = i
setTimeout(()=>{
console.log(x)
})
}
|
3)可以打印出 0、1、2、3、4、5
的方法汇总
试图让变量 i
执行看起来“符合人们的对逻辑(的幻想)”
1.利用 ES 6
方法
具体代码实现见上文
for
循环头部的 let
声明还会有一个特殊的行为。
- 这个行为指出变量在循环过程中不止被声明一次
- 每次迭代都会重新声明一次
i
,然后保存在,然后销毁
- 随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
之后全是ES6
前的方法,所以原本想只用var
,但是考虑到var
可能引起的问题,还是老实地用let
两者都可通过在浏览器控制台打出代码验证有效
以下都是变通方法,将要要消耗更多资源,见翻译不完整的MDN window.setTimeout
2.用闭包与自执行函数
1
2
3
4
5
6
7
8
|
let i = 0
for(i = 0;i<6;i ++) {
(function(i){
setTimeout(function() {
console.log(i)
}, 0 /* 想慢点就改成 `i * 1000 ` */);
})(i);
}
|
使用匿名函数包裹你的回调函数
利用函数创建一个局部变量,保留了调用时的局部变量 i
ES6
引入更简洁的箭头函数,可完全代替这里的所有匿名函数
箭头函数 声明的匿名函数
1
2
3
4
5
6
7
8
|
let i = 0
for(i = 0;i<6;i ++) {
(function(i){
setTimeout(( /* 这里不能瞎写i,我试过,传入形参就相当于重新声明,会覆盖掉函数体里的全局变量 */ )=> {
console.log(i)
}, 0);
})(i);
}
|
3.利用 bind 函数
1
2
3
4
5
6
|
let i = 0;
for (i = 0; i < 6; i++) {
setTimeout(function(i) { //这里使用匿名函数,具名函数也可,只是得不偿失
console.log(i)
}.bind(null,i), 0 )
}
|
- 如果
bind
方法的第一个参数是 null
或 undefined
- 等于将
this
绑定到全局对象
- 函数运行时
this
指向顶层对象(浏览器为 window
)
- bind 还可以接受更多的参数,将这些参数绑定原函数的参数
4.利用 setTimeout
的第三个参数
1
2
3
4
5
6
|
let i = 0;
for (i = 1; i < 6; i++) {
setTimeout(function(i) { //这里使用匿名函数
console.log(i)
}, 0, i)
}
|
setTimeout
函数的第三个参数及以后的参数,都可以作为传入setTimeout
的匿名函数函数的参数
其他参考文章与相关文章
MDN 合作异步JavaScript: 超时和间隔
网道 / WangDoc.com 异步操作概述
从setTimeout和Promise比较js执行机制
JS setTimeout 问题 贺师俊的回答
Event Loop、计时器、nextTick By 方应杭
JSConfEU的一个油管视频解释了一部分原理
正文开始
结构看起来不像对象,但函数,就只是一种内置对象
四种方法定义一个函数
基本语法
1
2
3
4
|
function 函数名(形式函数1,形式函数n){
语句
return 返回值
}
|
定义匿名函数
- 上面的具名函数,去掉函数名就是匿名函数
- 而
let a = function(x,y){return x+y}
- 也叫函数表达式,注意函数表达式是指
=
右边的部分
1
2
3
4
5
6
7
8
9
10
11
|
function fn(x,y){
return x+y
}
let a = function(x,y){
return x+y
}
let a1 = function fn1(x,y){
return x+y
}
fn1() // 报错
|
函数表达式中,如果函数fn()
在等号右边,那么fn()
的作用域就只在等号右边,超过函数体,fn()
就不存在,就只能用a1()
没有等于号,作用域就在当前作用域的全局下
用ES6箭头函数
箭头函数,箭头左边就是输入参数,右边就是输出参数
两个或两个以上输入或输出,用括号括起来,而且有返回值的话,不能省略return
箭头右边有两个或两个以上语句,则要用花括号括起来
1
2
3
4
5
6
7
|
let f1 = x => x*x
let f2 = (x,y) => x+y //形参的圆括号不可省略,没有返回值的话,花括号可以省,并且必须省略`return`
let f3 = (x,y) => {
console.log('hi')
return x+y
} //有返回值,花括号不可省略
let f4 = (x,y) => ({name:x, age:y})
|
箭头函数字节返回对象会出错,需要加个圆括号
JS中,{}
优先被当做代码块,即{
当做块的起始
1
2
3
4
5
|
let f4 = x => {name:x} // 语法错误
let f5 = x => ({name:x}) // 语法对了,就显得有些Zz
f4('frank') // undefined ,JS解释器认为是一个label 标签语句
f5('frank') // {name: "frank"}
|
用构造函数
1
|
let ff = new Function('x','y','return x+y')
|
- 基本没人用,但是能让你知道函数时谁构造的
- 所有函数都是
Function
构造出来的,是JS世界的创世神
- 包括
Object
、Array
、Function
自身也是
函数自身 V.S. 函数调用
fn V.S. fn()
函数自身
代码
1
2
|
let fn = () => console.log('hi')
fn
|
结果
函数调用
代码
1
2
|
let fn = () => console.log('hi')
fn()
|
结果
再进一步
fn
和fn()
本质
1
2
3
|
let fn = () => console.log('hi')
let fn2 = fn
fn2()
|
结果
let fn = () =>({})
表示fn
保存了匿名函数的地址
let fn2 = fn
表示这个地址被赋值给了fn2
fn2()
调用了匿名函数
fn
和fn2
都是匿名函数的引用而已
- 真正的函数既不是
fn
也不是fn2
,而是存在内存中的代码() => console.log('hi')
函数的要素
每个函数都有这些独特的属性
- 调用时机
- 作用域
- 闭包
- 形式参数
- 返回值
- 调用栈
- 函数提升
arguments
(除了箭头函数没有)
this
(除了箭头函数没有)
下面开始逐一初步解释
执行时机
函数调用时机不同,结果不同,联想刻舟求剑
例1:
1
2
3
4
|
let a = 1
function fn(){
console.log(a)
}
|
问打印出多少
答:不打印,因为没有调用代码
例2:
1
2
3
4
5
|
let a = 1
function fn(){
console.log(a)
}
fn()
|
问打印出多少
答:打印1
例3:
1
2
3
4
5
6
|
let a = 1
function fn(){
console.log(a)
}
a = 2 //此时,fn()没有执行,不要刻舟求剑,a已经被赋了新的值2
fn()
|
问打印出多少
答:打印2
1
2
3
4
5
6
|
let a = 1
function fn(){
console.log(a)
}
fn()
a = 2
|
问打印出多少
答:打印1
1
2
3
4
5
6
7
8
9
10
11
12
|
let a = 1
function fn(){
setTimeout(()=>{
console.log(a)
},0)
// 在一段时间后,执行,多少时间根本不是关键
// 而是setTimeout()里的代码的执行时机
// 要等后续代码执行完后,再执行setTimeout()
// 期间setTimeout()里的所有变量都受到后续代码的影响
}
fn()
a = 2 // 打印的时机是在 a = 2 之后
|
问打印出多少
答:打印2
1
2
3
4
5
6
|
let i = 0
for(i = 0; i < 6; i++){
setTimeout(()=>{
console.log(i)
},0)
} // 先执行完setTimeout()外的代码,这里是for循环,最终 i = 6,之后setTimeout()执行6次
|
问打印出多少
答:打印6个6,而不是0、1、2、3、4、5
1
2
3
4
5
|
for(let i = 0; i < 6; i++){
setTimeout(()=>{
console.log(i)
},0)
}
|
问打印出多少
答:打印0、1、2、3、4、5
- 因为JS在
for
和let
一起使用的时候会加料
- 每次循环多创建一个
i
,留在setTimeout()
里,保留i
的值,不跟随新的i
值变化
- 即生成6个新的
i
,加上原本的初始,一共有7个i
- 刻舟求剑实现啦
1
2
3
4
5
6
7
8
9
10
11
12
13
|
var i = 0
function print() { // 用函数作用域包裹
var p = i
console.log('p: '+p, 'i: '+i)
setTimeout(() => {
console.log(p)
},0)
console.log('p: '+p, 'i: '+i)
}
for (i = 0; i < 6; i++){
console.log('i: '+i)
print(i)
}
|
每次求值、取值的时候,都要想想执行时机,代码顺序,值可能是以前的,错的或是非预期的,可用console.log
确认
1
2
3
4
5
6
7
8
9
|
let i = 0
function print(i) {
setTimeout(() => {
console.log(i)
},0)
}
for (i = 0; i < 6; i++){
print(i)
}
|
使用闭包 自执行函数
作用域
每个函数都会默认创建一个作用域
例1:
1
2
3
4
|
function fn(){
let a = 1 // let 的作用域就是离左边最近不成对的 '{',离右边最近不成对的 '}',两者之内的区域
}
console.log(a) //a不存在
|
问:是不是因为fn
没执行导致
答:就算fn
执行了,也访问不到作用域里面的变量a
fn
执行完了,作用域内的变量就被JS垃圾回收机制干掉了
例2:
1
2
3
4
5
|
function fn(){
let a = 1
}
fn()
console.log(a) //a还是不存在
|
全局变量 V.S. 局部变量
1
2
3
4
5
6
|
let b = 1
window.c = 2
function f1(){
console.log(c)
}
f1()
|
1.在顶级作用域声明的变量是全局变量
2.window
的属性是全局变量,不管你在什么地方定义变量,挂在window
上的自动提升为全局变量
1
2
3
4
5
6
7
8
9
|
let b = 1
function f2(){
window.c = 2
}
f2() //要执行,才能把c挂到window上
function f1(){
console.log(c)
}
f1() //2 在f1内可以访问到c
|
- 包括
window.Object
、window.Array
、window.Function
、window.parseInt()
等都是全局变量,哪里都可以用
除了这两种情况,其他都是局部变量
函数可嵌套,作用域也可嵌套
例3:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function f1(){
let a = 1
const b = 0
function f2(){
let a = 2
console.log(a)
} // f2()
console.log(a)
a = 3 //就是唬你的
f2()
} // f1()
f1() //f1()也是唬你的
console.log('a:' + a)
|
作用域规则
如果多个作用域有同名变量a
- 查找
a
的声明时,向上取最近的作用域,简称「最近原则」
- 干掉(无视)此作用域中嵌套的作用域
- 查找
a
的过程与函数执行无关
- 但
a
的值与函数执行有关
例4:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
function f1(){
let a = 1
function f2(){
let a = 2
function f3(){
console.log(a) //向上取最近的作用域,即f2()中,let a = 2
}
a = 22
f3() // 当要确定`a`的值的时候,才与函数执行有关
}
console.log(a)
a = 100
f2()
}
f1()
|
和函数执行无关的作用域,成为静态作用域,也叫词法作用域,JS目前(2020)只有这一类作用域,涉及到编译原理的知识
闭包
上文已经包含了「闭包」(Closure),变量a
和函数f3
形成了闭包
JS函数会就近寻找“最近的”变量,这就是闭包~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
function f1(){
let a = 1
function f2(){
====================
let a = 2
function f3(){
console.log(a) //f3用到了外部的变量a,那么,这个f3加上a,就形成「闭包」
}
====================
a = 22
f3()
}
console.log(a)
a = 100
f2()
}
f1()
|
问:什么是闭包
答:如果一个函数用到了外部的变量,那么,这个函数加上,这个变量,就叫做「闭包」
作用以后介绍,现在只说定义
- 「函数」和「函数内部能访问到的变量」(也叫环境)的总和,就是一个闭包。
闭包的用途
看这篇文章
JS 中的闭包是什么?
举例:下例中,闭包在哪
1
2
3
4
5
6
|
for (let i = 0; i < 6; i++) {
setTimeout(() => {
console.log(i)
}, 0)
console.log('我先运行了第' + i + '次 i')
}
|
1
2
3
4
5
6
7
8
9
10
|
let i = 0
function doSetTimeout(i) {
setTimeout(() => {
console.log(i)
}, 0)
}
for (i = 0; i < 6; i++) {
doSetTimeout(i)
console.log('我先运行了第' + i + '次 i')
}
|
形式参数
形式参数就是非实际参数
1
2
3
|
function add(x,y){
return x+y
}
|
- 其中
x
和y
就是形参,因为并不是实际的参数(指定了调用时传入函数的参数的顺序)
- 调用
add(1,2)
时,1
和2
就是实际参数,会被分别按次序赋值给x
和y
形参的本质是变量声明
1
2
3
4
5
6
|
//上面的代码近似等价于下面的代码
function add(){
var x = arguments[0]
var y = arguments[1]
return x+y
}
|
形参只是语法糖
形参可多可少,只是给参数取名字,比较随意,形参类型无语法层面上的限制,和TypeScipt不同
“值传递”和“地址传递”?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
function add(x,y){
return x+y
}
add(1,2) //3
function add2(x){
return x + arguments[1]
}
add2(1,2)
/* 将内存里Stack保存的地址,传给形参 */
let a = {value:1} //内存里Stack保存的地址
let b = {value:2}
function addObject(x,y){ //内存里Stack保存的地址拷贝给x,y
x.name = 'xxx' //改变对象的属性,根据内存图全部复制
return x.value + y.value
}
addObject(a,b)//3
|
返回值
每个函数都有返回值
1
2
3
4
|
function hi(){
console.log('hi')
}
hi()
|
没写return
,所以返回值是undefined
1
2
3
4
|
function hi(){ // 不执行,就不存在返回值
return console.log('hi')
}
hi() //undefined
|
返回值为console.log('hi')
的值,console.log()
返回值仍为undefined
,打印值和返回值不同
小结
作用域:就近原则 & 闭包
调用栈
函数执行时要进入一另个环境,执行完,再返回回来
有时函数执行,进入嵌套函数,返回路径是怎样
调用栈就是记录函数执行完后,回去到哪里
什么是调用栈(Call stack)
- JS引擎在调用一个函数前
- 需要把函数所在的环境
push
到一个数组里
- 这个数组叫做调用栈
- 等函数执行完了,就会把环境
pop
出来
- 然后
return
到之前的环境,继续执行后续代码
举例
1
2
3
|
console.log(1)
console.log('1+2的结果为' + add(1,2))
console.log(2)
|
执行console.log(1)
的过程:进入log函数
,压入栈,调用栈记录回到第1行
(地址)->打印1
->弹栈,读取调用栈->回到第1行
执行console.log('1+2的结果为' + add(1,2))
的过程:压栈,调用栈记录回到第2行/21列
->执行add(1+2)
->压栈,计算结果3
->弹栈,回2/21
->得到字符串'1+2'的结果为3
->进入console.log
函数,压栈,调用栈记录回到第2行
->打印字符串'1+2'的结果为3
->弹栈,读取调用栈->回到第2行
- 每次调用函数,都要记录一次返回地址,写到调用栈里,如果还要进入嵌套的函数,调用栈再次记录里一个返回地址,等函数执行完,就弹栈,回去记录的返回地址
Call stack(调用栈)MDN的解释
- 1.函数将要执行
- 2.压栈(记录执行的函数到调用栈)
- 3.执行函数体代码,执行完了
- 4.弹栈(删除调用栈中的函数)
- 如果有嵌套的函数,重复执行
1234
递归函数
使用递归函数有可能把栈压满,即爆栈
1
2
3
|
function f(n){
return n !== 1 ? n*f(n-1) : 1
}
|
理解递归
f(4)
= 4 * f(3)
= 4 * (3 * f(2)
= 4 * (3 * (2 * f(1)))
= 4 * (3 * (2 * (1)))
= 4 * (3 * (2))
= 4 * (6)
= 24
递归函数的调用栈
画出阶乘(6)的调用栈
压6次栈,弹6次栈(递进6次,回归6次)
1
2
3
4
5
6
7
8
|
function computeMaxCallStackSize(){
try{
return 1 + computeMaxCallStackSize();
} catch (e){
//报错说明stack overflow了
return 1;
}
}
|
Chrome 12578
Firefox 26773
Node 12536
Safari 31713
爆栈:如果调用栈中压入的帧过多,程序就会崩溃
函数提升
什么是函数提升
function fn(){}
- 不管吧具名函数声明在哪里,都会提升到第一行
什么不是函数提升
let fn = function(){}
- 函数表达式是赋值,右面的函数声明不会提升
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
add() //fine
function add(){}
let add1 = 1 //let 不允许重复声明
function add1(){} //SyntaxError: redeclaration of let add1 ,声明提前,和let声明的add1重复
var add2 = 1 //var 允许重复声明,所以有风险,不可用
function add2(){}
var add3
function add3(){} //>add3 <f add3(){}
fucntion add4(){}
var add4 = 1 //>add3 <1
add5()
let add5 = function(){} //报错
|
arguments
和this
(除了箭头函数没有)
代码
1
2
3
4
5
6
7
8
9
10
|
function fn(){
console.log(arguments)
console.log(this)
console.log(Array.from(arguments)) //变为真·数组
}
fn(1)
fn(1,'b')
fn.call(1)
fn.call('xxx') //'xxx'会被自动转化(封装)成对象,即包装对象
|
如何传arguments
- 包含所有参数的伪数组
- 调用
fn
即可传值给arguments
fn(1,2,3)
,那么arguments
就是[1,2,3]
的伪数组
- 没有JS数组的方法,
__proto__
的构造函数是Object
(原型链直接指向Object.prototype
的地址)
- 用
Array.from(arguments)
变为真·数组
代码
1
2
3
4
5
6
7
8
9
10
|
function fn(){
/* 'use strict ' */ //强制 this 传值时 不封装为对象
console.log('this:')
console.log(this)
}
fn() //没有其他任何条件,this 默认指向window
fn.call(1) // 返回Number对象
fn.call('xxx') // 返回String对象 //'xxx'会被自动转化(封装)成对象,即包装对象
fn.call(undefined) //默认window
|
如何传this
- 目前可以用
fn.call('xxx',1,2,3)
传this
和arguments
,其中xxx
,就是传给this
的值,后面的参数会被传给arguments
- 而且
xxx
会被自动转化(封装)成对象,即包装对象,例如Number {1}
、String {''}
- 除非在
fn
代码块顶部写一句use strict
,但会带来许多其他问题
- 这是JS的糟粕之一
this
默认指向window
(控制台打出的是Window
),一般不需要
this
是隐藏参数,arguments
是普通参数
this
是参数,个人观点
假设没有this
1
2
3
4
5
6
|
let person = {
name: 'You',
sayFuck(){
console.log(`您好,我叫` + person.name)
}
}
|
分析
- 我们可以用直接赋值(保存了对象地址)的变量获取
name
属性
- 我们可以吧这种办法简称为引用
1
2
3
4
5
6
7
|
let sayHi = function(){
console.log(`您好,我叫` + /*person.*/name)
}
let person = {
name: 'frank',
'sayHi': sayHi
}
|
分析
- 用函数表达式,变量名未知
person
如果改名,sayHi
函数就挂了
sayHi
函数甚至有可能在另一个文件里面
- 所以不希望
sayHi
函数里出现person
引用
问题二
1
2
3
4
5
6
7
8
|
class Person{
constructor(name){
this.name = name //这里的this是new强制指定的
}
sayHi(){
console.log(/* ??? */) //没有实例化,无法引用
}
}
|
分析
- 这里只有类,还没创建对象,所以不可能获取对象的引用
- 那么如何拿到对象的
name
属性?
需要有一种办法拿到对象,这样才能获取对象的name
属性
对象
1
2
3
4
5
6
7
|
let person = {
name: 'frank',
sayHi(p){
console.log(`您好,我叫` + p.name)
}
}
person.sayHi(person) //丑
|
类
1
2
3
4
5
6
7
|
class Person{
constructor(name){ this.name = name}
sayHi(p){
console.log(`您好,我叫` + p.name)
}
}
let person = new Person(person) //丑
|
Python
就用了
1
2
3
4
5
6
7
8
9
10
11
|
class Person:
def __init__(self, name): # 构造函数 声明一个self 参数,即未来声明新对象
self.name = name
# 回车
def sayHi(self):
print('Hi,I am' + self.name)
person = Person('frank')
person.sayHi() # 自动传参 self
|
特点
- 每个函数都接受一个额外的
self
- 这个
self
就是传入的新对象
- 只不过
Python
会偷偷帮你传对象
Python
中person.sayHi()
等价于person.sayHi(person)
Python
中person
就被传给self
了
解决的问题是:怎么得到未来要创建的新对象的引用
JS没有模仿Python
的思路,走了另一条更难理解的思路
JS在每个函数里加了this
1
2
3
4
5
6
|
let person = {
name: 'frank',
sayHi(/* this */){ //JS 也自动把this 传给函数sayHi
console.log(`你好,我叫` + this.name)
} //淘宝号的this 接受了person
}
|
分析
JS
中person.sayHi()
相当于person.sayHi(person)
- 然后
person
被自动传给this
了(person
是个地址)
- 这样,每个函数都能用
this
来获取一个 未知对象的引用 了
person.sayHi()
会隐式地把person
作为this
传给sayHi
方便sayHi
获取person
对应的对象
this
小结
- 想让函数获取未知对象的引用
- 但是并不想通过变量名做到,变量名未知
Python
通过额外地self
参数做到
JS
通过额外的this
做到
1.person.sayHi()
会把person
自动传给sayHi
2.sayHi
可以通过this
引用person
其他
- 注意
person.sayHi
和person.sayHi()
的区别
- 注意
person.sayHi()
的断句(person.sayHi)()
class也一样
1
2
3
4
5
6
7
|
class Person{
sayHi(){
console.log(this.name)
}
}
let p = new Person()
p.sayHi() // JS引擎自动把 this = p
|
这就引出另一个问题
1
2
3
4
5
6
7
8
|
let person = {
name: 'frank',
sayHi( /* this */ ){
console.log(`你好,我叫` + this.name)
}
}
person.sayHi()
person.sayHi(person)
|
传到哪里了
person.sayHi()
对,JS怎么解决这种不和谐
函数两种调用模式
小白调用法
会自动把person
传到函数里
1
|
person.sayHi() //隐藏太多细节,不清不楚
|
老油条调用法
1
|
person.sayHi.call(person)
|
该学那种?默认用第二种
1
2
3
4
5
6
7
8
9
10
|
let person = {
name : 'frank',
sayHi(){
console.log(this.name)
}
}
person.sayHi.call({name:1}) //1 //使得调用变得清晰
person.sayHi.call({name:'Jack'}) //Jack
person.sayHi.call(person) //frank
|
- 为了记忆,之后调用函数都用
object.fn.call()
写法
例1
未用到this
1
2
3
4
|
function add(x,y){
return x+y
}
add.call(undefined,1,2) // 3
|
为什么要多写一个undefined
- 因为第一个参数要作为
this
- 但是代码里没有
this
- 所以只能用
undefined
占位
- 其实用
null
也可以
例2
1
2
3
4
5
6
|
Array.prototype.forEach2 = function(){
console.log(this)
}
let arr = [1,2,3]
arr.forEach2.call(arr) //[1,2,3]
arr.forEach2() //[1,2,3]
|
arr.forEach2.call(arr)
等价于arr.forEach2()
完整版模拟forEach()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
Array.prototype.forEach2 = function(fn){
for(let i = 0; i < this.length; i++){
fn(this[i],i,this)
console.log('this[i]: ' + this[i])
console.log('i: ' + i)
console.log('this: ' + this)
console.dir(this)
}
}
let arr = [1,2,3]
arr.forEach2.call(arr,(item)=>console.log(item)) // 手动把arr作为this,手动[滑稽]
/* 等价于 */
arr.forEach((item)=>console.log(item)) // 自动把arr作为this
Array.prototype.forEach.call({0: '1', 1: 'b',length:2})
Array.prototype.forEach2.call({0:'a',length:3},(item)=>console.log(item))
Array.prototype.forEach2.call({0:'a',length:1},(item)=>console.log(item))
|
this
是什么
- 一回答就错了,还没有传值,只有在
call(xxx)
的时候才能确定,所以不知道this
是什么
- 由于大家使用
forEach2
的时候总是会用arr.forEach2
- 所以
arr
就被自动传给forEach2
,作为this
this
一定是数组吗
- 不一定,比如
Array.prototype.forEach.call({0: '1', 1: 'b',length:2})
这样的伪数组
this
只是一个可以任意指定的参数而已
1
2
|
[].forEach2.call({0:'a',length:1},(item)=>console.log(item))
[].forEach2.call([{},[],'',NaN,undefined,null],(item)=>console.log(item))
|
this
的两种使用方法
1
2
3
4
5
6
|
fn(1,2) //等价于 fn.call(undefined,1,2)
fn.call(undefined,1,2)
/* 如果是对象的属性的函数,this就传此对象的此属性 */
obj.child.fn(1) //等价于 obj.child.fn.call(obj.child,1)
obj.child.fn.call(obj.child,1)
|
不管用什么方式调用函数,都传递一个this对应的值
问题在于,是否知道传的this是什么
1
2
|
fn.call(undefined,1,2)
fn.apply(undefined,[1,2]) //参数要用数组的形式传值
|
强制绑定this
如果不能确定this到底是什么
1
2
3
4
5
6
|
function f1(p1,p2){
console.log(this,p1,p2)
}
let f2 = f1.bind({name: 'frank'}) // f2就是 f1 绑定了 this 之后的新函数
f2() // 调用f2,就等价于 f1.call({name: 'frank'})
f1.call({name: 'frank'}) //f2是f1强制绑定了参数{name: 'frank'}的版本
|
1
2
3
4
5
6
7
|
function f1(p1,p2){
console.log(this,p1,p2)
}
let f3 = f1.bind({name: 'frank'}, 'hi') // this是{name: 'frank'},第一个参数是'hi',已经绑死了
f3() //等价于 f1.call({name: 'frank'}, 'hi')
f3('fuck') // Object { name: "frank" } hi fuck
//之后传入的形式参数就从第二个开始传了
|
箭头函数
没有arguments
和this
- 箭头函数的this就是普通变量
- 里面的
this
就是外面的this
1
2
3
4
5
6
7
8
9
10
|
console.log(this) //window
console.log(this === window) //true
let fn = () => console.log(this)
let b = 1
let fn = () => console.log(b)
console.log(this)
let fn2 = ()=> console.log(this)
fn2()
|
1
2
3
4
5
6
7
|
let fn = () => console.log(this)
fn.call({name: 'frank'}) //window
fn() //window
fn.call(1) //window
let fn3 = ()=> console.log(arguments)
fn3(1,2,3)
|
总结
每个函数都有这些独特的属性
- 调用时机
- 作用域
- 闭包
- 形式参数
- 返回值
- 调用栈
- 函数提升
arguments
(除了箭头函数没有)
this
(除了箭头函数没有)
立即执行函数
只有JS有的变态玩法,ES6后用得少
原理
- ES5时代,为了得到局部变量,必须引入一个函数
- 但是这个函数如果有名字,就声明了一个全局函数,得不偿失
- 于是这个函数必须是匿名函数
- 声明匿名函数,然后立即加个
()
执行它
- 但JS标准认为这种语法不合法
- 最终发现,只要在匿名函数前加个运算符即可
!
、~
、()
、+
、-
都可以
- 往上走
- 只用
!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
var a = 1 // 全局变量
function fn(){ // 全局函数
var a = 2 // 局部变量
console.log(a)
}
console.log(a)
fn()
/* 声明一个匿名函数 直接调用 */
function(){
var a = 2
console.log(a)
} () //报错
!function(){
var a = 2 // 局部作用域
console.log(a)
} () //true 不报错 虽然返回了一个不需要的值
/* 带来的问题 */
console.log('hi') // undefined
(function(){console.log('hi')})() // JS里回车没有意义,相当于 undefined(function(){console.log('hi')})()
/* 除非加个“;” ,也是JS里少数几个必须加分号的地方 还有 `([]) !== ([]);({}) !== ({});`等设计圆括号的 */
console.log('hi')
;(function(){
var a = 2 // 局部作用域
console.log(a)
} )()
/* ES6 局部变量 */
{
let a = 3
console.log(a)
}
console.log(a)
|
关于 this
这篇Blog初步讲了 this,不过相信 this 作为 JS 的第二座大山,不是那么容易能理解的。
前期以记忆为主,后面几节才有可能真正理解 this。
从下节开始,所有函数调用使用 call 的形式,直到理解 this
参考文章
相关文章
- 作者: Joel
- 文章链接:
- 版权声明
- 非自由转载-非商用-非衍生-保持署名
- 河
掘
思
知
简