JS函数和你不知道的this

四种方式定义函数

具名函数

function 函数名(形式参数1, 形式参数2) {
	语句
	return 返回值
}

匿名函数

  • 上面的具名函数,去掉函数名就是匿名函数
  • let a = function(x, y) {return x+y}
  • 也叫函数表达式

箭头函数

let f1 = x => x*x
let f2 = (x, y) => x+y // 圆括号不能省
let f3 = (x, y) => {return x+y} //花括号不能省
let f4 = (x, y) => ({name:x, age: y})

直接返回对象会出错,需要加个圆括号

构造函数

  • let f = new Function(‘x’, ‘y’, ‘return x+y’)
  • 所有函数都是Function构造出来的
  • 包括Object、Array、Function也是

基本没人用,但是能让你知道函数是谁构造的

函数的要素

  • 调用时机
  • 作用域
  • 闭包
  • 形式参数
  • 返回值
  • 调用栈
  • 函数提升
  • arguments(除了箭头函数)
  • this(除了箭头函数)

调用时机

来看三个例子,看看JS有多操蛋。

let a = 1
function fn(){
	setTimeout(()=>{
		console.log(a)
	}, 0)
	fn()
	a = 2
}

打印多少? 2

let i = 0
for(i = 0; i < 6; i++){
	setTimeout(()=>{
		console.log(i)
	}, 0)
}

打印多少? 不是0,1,2,3,4,5.而是6个6。循环到每一个i都是过一会打印出i。过一会就是指要把所有循环走完。那么当循环走完,i就为6,所以要打印出6个6

for(let i = 0; i < 6; i++){
	setTimeout(()=>{
		console.log(i)
	}, 0)
}

打印多少?

是0,1,2,3,4,5. 因为JS在for和let一起用的时候会加东西。每次循环会多复制一个i留在那里,不跟随新的i变化。(彳亍口巴)。

那这样setTimeout还有什么意义?

好问题,🈚️意义。

作用域:就近原则 & 闭包

function fn(){
	let a = 1
}
fn()
console.log(a) // a不存在

全局变量:

window.c = 1

如果多个作用域有同名变量a, 那么查找a的声明时,就向上取最近的作用域,查找a的过程与函数执行无关,但a的值与函数执行有关。

和函数执行没有关系的作用域叫做静态作用域(词法作用域)。和函数执行有关系的叫做动态作用域,JS🈚️动态作用域。

最后看个例子来理解作用域:

如果一个函数用到了外部的变量,那么这个函数加这个变量就叫做闭包。上面代码4-7行中的a和f3就组成了闭包。

形式参数和返回值

  • 形参可认为是变量声明,类似于:
function add(){
	var x = arguments[0]
	var y = arguments[1]
	return x+y
}
  • 没return返回值是undefined

递归、调用栈与爆栈

什么是调用栈

  • JS引擎在调用一个函数前
  • 需要把函数所在的环境push到一个数组里
  • 这个数组叫做调用栈
  • 等函数执行完了,就会把环境pop出来
  • 然后return到之前的环境,继续执行后续代码

Call stack(调用栈) | MDN

递归函数

// 阶乘
function f(n){
	// return n === 1 ? 1 : n * f(n-1)
	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

先递进,再回归

想想递归的调用栈(压栈和弹栈):

爆栈

如果调用栈中压入的帧太多,程序就会崩溃。

调用栈大小: Chrome 12578, Firefox 26773, Node 12536(Node和Chrome用的JS引擎都是V8,所以差不多)

函数提升

不管具名函数声明在哪,都会跑到第一行

arguments和this

  • arguments是一个包含该函数所有普通参数的伪数组
  • 调用当前函数,this默认表示window
  • 加上’use strict'就不会让this自动将传入的参数转化为对象了 (没人这么做)

实际上this是第一个参数,arguments是从第一个参数往后的所有参数。

this是隐藏参数,arguments是普通参数(个人结论)

person.sayHi()会隐式地把person作为this传给sayHi

两种调用法

  • 小白调用法
person.sayHi()
// 会自动把person传到函数里,作为this
  • 大师调用法
person.sayHi.call(person)
// 需要自己手动把person传到函数里,作为this
  • 例子
function add(x,y) {
	return x +y
}
// 不用this
add.call(undefined, 1, 2) // 传undefined是占位,因为第一个参数要作为this。

用this:

上面的例子,用array作为this,调用了forEach2

两种写法再探究:

一个是显示地指定array是this,一个是隐式地指定array是this。

总结一下this的两种使用方法

  • 隐式传递
fn(1,2) //等价于fn.call(undefined, 1, 2)
obj.child.fn(1) //等价于obj.child.fn.call(obj.child,1)
  • 显式传递
fn.call(undefined, 1, 2)
fn.apply(undefined, [1,2])

绑定this

  • 使用.bind可以让this不被改变
function f1(p1, p2){
console.log(this, p1, p2)
}
let f2 = f1.bind({name:'frank'})
// 那么f2就是f1绑定了this之后的新函数 
f2() // 等价于f1.call({name:'frank'})

就调f2相当于调f1,唯一的区别是你把this绑定了

有什么用? Vue和React中会经常用到。

  • .bind还可以绑定其他参数
let f3 = f1.bind({name:'frank'}, 'hi')
f3() // 等价于f1.call({name:'frank'}, hi)

箭头函数

箭头函数没有arguments和this

  • 里面的this就是外面的this
console.log(this) //外面的this是window
let fn = () => console.log(this)
fn() // window
  • 就算你加call都没有用
fn.call({name:'frank'}) // window,没有用,指定什么都没有用,还是window

就如同:

let b = 1
let fn = () => console.log(b)

b是外面的b,this也是外面的this(window)

  • 同样的,箭头函数没有arguments,舒服:
let fn = () => console.log(arguments)
fn(1,2,3)
// Uncaught ReferenceError: arguments is not defined at fn

立即执行函数(生成局部变量的方法)

只有JS有的变态玩意,现在用得少

原理

  • ES5时代,为了得到局部变量,必须引入一个函数
  • 但是这个函数如果有名字,就还会引入一个全局函数,得不偿失
  • 于是这个函数必须是匿名函数
  • 声明匿名函数,然后立即加个()执行它
  • 但是JS标准认为这种语法不合法
  • 所以JS程序员寻求各种办法
  • 最终发现,只要在匿名函数前面加个运算符即可
  • !、~、()、+、-都可以
  • 但是这里面有些运算符会往上走
  • 所以推荐永远用!来解决
//ES5老方法
!function (){
	var a =2
	console.log(a)
} ()
//ES6最新方法
{
    let a = 2
}
//不推荐()
console.log('hi')  //如果立即执行函数前面这句代码
(function (){
	var a =2
	console.log(a)
} ()) //Uncaught TypeError:console.log(...) is not a function


//因为js中回车没意义,所以把下面的立即执行函数接到前面去了,所以相当于
undefined(function (){
	var a =2
	console.log(a)
} ()) //把undefined当一个函数来执行了,哪来的undefined这个函数,所以报错


//补救措施
console.log('hi');  //在他俩之间加分号。注意这是js语言中唯一两句代码之间需要加分号的情况
(function (){
	var a =2
	console.log(a)
} ())

结论,你就用ES6语法就完事了。

comments powered by Disqus