JS函数 调用栈 函数提升

1,534 阅读9分钟

立即执行函数 IIFE

远古时代的产物,为了造一个局部变量

// 几种写法中最推荐的是感叹号加减号的,因为就算前文的代码没写分号,依旧可以正常运行
(function(){})() 
(function(){}()) 
!function test1(){}()//感叹号加号减号把函数申明变成函数表达式 
+function test2(){}()
-function test3(){}()

声明函数的几种方式

function fn(){}; // 1
var fn=function(){}; // 2
var a=function fn(){}; // 3 注意这个比较特殊
fn() // error 执行3的fn会报错

箭头函数

如何优雅的在箭头函数内返回一个对象呢?

// 如何优雅的返回一个对象呢?
var fn=()=>{name:'ryan'}; // 错误 js会把它翻译成block而不是对象
var fn=()=>({name:'ryan'}); // 外面套上(),足够优雅

箭头函数内没有自带的arguments与this

  • arrow内部没有this,会由内向外找this
console.log(this); // this -> window
var fn=()=>console.log(this); // this -> window
fn();
var fn=function(){ 
	console.log(this) // 不用去猜测this指向,在未经调用时,this都是不明的
    
    var fn1=function(){ // 非arrow
        console.log(this);
    }
    fn1(); // 最普通的函数调用,this ---> window
    fn1(undefined); // 传入undefined,this ---> window

    var fn2=()=>{ // arrow
        console.log(this);
    }
    fn2(); // 箭头函数自身没有this,用的是外面的this
}
fn.call({name:'ryan'});
  • 如何理解箭头函数自身是没有this的?因为压根没有,所以无法被改变
// 即使arrowFunction.call强制改变this,也无法改变arrow自身没有this,this来自外部的事实
console.log(this); // this -> window
var fn=()=>console.log(this); // this -> window
fn.call({name:'ryan'}); // 试图改变arrow this

for let

  • 我先证明for let的变量仅存在于block中
function fn(){
	for(let i=0;i<5;i++){
    	// 相当于 let i
    	if(i===2){
        	break;
		}
    }
    return i // 出了block根本获取不到i
}
fn(); // error 提示说根本没有i

// 例子1
let i;
for(i=0;i<5;i++){ // i是全局i
	setTimeout(()=>{
    	console.log(i);
    },0)
} // 5个5
// 例子2
for(var i=0;i<5;i++){ // i是全局i
	setTimeout(()=>{
    	console.log(i);
    },0)
} // 5个5
// 例子3
for(let i=0;i<5;i++){ // i是block i
	setTimeout(()=>{
    	console.log(i);
    },0)
} // 0 1 2 3 4

全局变量与隐式全局变量

var aa=0; //全局变量
console.log(window.aa) // 0
let a=0; //也是全局变量 但是window上找不到
console.log(window.a) // undefined
function fn1(){
	window.b=0; // 声明全局变量b 虽然是写在函数作用域内
    c=100; // 等价于window.c=100, 这也叫隐式全局变量
}
fn1()
console.log(a,window.b,window.c)

静态作用域

作用域的确定与函数的执行没有任何关系的作用域

词法作用域

词法这个概念是编译原理的知识 作用域的确定与函数的执行有关系的作用域叫词法作用域

闭包

  • 函数访问外部变量叫闭包
  • 如果一个函数用到了外部的变量,那么这个函数加这个外部变量就叫做闭包

参数传递

// 值传递与地址传递 都是一回事
var a={value:1};
var b={value:2};
var addValue=function(x,y){
	x.name='ryan';
	return x.value+y.value
}
addValue(a,b); // 3
console.log(a); // {value: 1, name: "ryan"}
// 形参的本质就是变量声明
function add(x,y){
}
// 等价于以下
function add(){
 var x=arguments[0]
 var y=arguments[1]
}

每一个函数都有返回值

function hi(){
	console.log('hi');
    // 没写return 返回undefined
}
hi() // undefined 返回值

调用栈

  • 调用栈记录了你进入一个函数以后出去去到哪里
  • JS引擎会在你调用一个函数以前把这个函数需要用到的环境推到一个数组里面,这个数组叫调用栈
  • 等到函数执行完JS会把环境pop出来然后return到之前的环境继续执行代码;

步骤如下:

  • 读到第一行console.log(1);
  • 进入console.log()函数
  • 把'回到第一行'这个信息压到调用栈中
  • 执行console.log(1)
  • 把'回到第一行'这个信息从调用栈中弹出
  • 回到第一行

为什么递归会让栈溢出?

递归:先递进再回归,而不是调用自己就是递归,死循环和递归不是一回事
举例一个递归函数: 阶乘

function fn(n){
 return n!==1?n*fn(n-1):1 
}

假设n为4,压栈与弹栈的过程如图:

  • 压栈4次,到底之后再依次弹栈
  • 每次压栈的时候都会记录位置以及n*的信息

函数提升

  • 匿名函数会留在原地
  • 有名函数会被提升到变量的前面
function fn(){} // 以此方式命名的函数,存在函数提升,并且是一整段的提升
var fn=function(){} // 匿名函数不会得到提升,仍然留在原处,但因为 var fn,所以var fn会被提到最顶部
let fn=function(){} // 匿名函数不会得到提升,仍然留在原处,同时 let fn,所以let fn也会留在原地
add(1,2)
function add(a,b){
 return a+b;
}
// 能运行的原因就是函数提升
let add=1;
function add(){}
// error 因为add函数会提let add=1;的上面,则let不允许再一次声明add

demo1

// demo1
getName()
var getName = function () {
 alert('closule')
}
function getName() {
 alert('function')
}
getName()
// function ---> closule
// demo1的解析
function getName() { //有名函数,整段向上提升
 alert('function')
}
var getName // 因为var所以变量提升
getName();
getName = function () { // 匿名函数留在原地
 alert('closule')
}
getName()

demo2

// demo2
console.log(a); 
console.log(a());
var a = 3;

function a() {
 console.log(10) 
}
console.log(a) 
a = 6;
console.log(a());
// 函数a---> 10 ---> undefined---> 3 ---> error
// demo2的解析
function a() { // 有名函数,整段提升,并且在变量之前
 console.log(10) // 无return则undefined
}
var a // 得到提升,但没有赋值,是不会影响函数a的
console.log(a); 
console.log(a());
a = 3;
console.log(a) 
a = 6;
console.log(a()); // 无法执行,不是函数
// 你注意
var a=0;
var a;
console.log(a) // 0  var只声明不赋值的话也是无法覆盖第一句的
// 不管你是哪个顺序,都是同名的函数在同名的变量的前面
var a 
function a() { 
}
console.log(a) // 打印出一个函数

function a() { 
}
var a 
console.log(a) // 打印出一个函数

arguments

  • arguments是包含所有参数的伪数组,Array.from去转成数组
function fn1(){
	console.log(arguments) // 哪怕你没写parameter,函数内依旧有arguments
}
fn1(1)

call与this

  • call函数显式地把this暴露出来,而不再是让JS隐式指定this
  • this多数情况默认指向 点 前调用它的那个对象
    • 普通函数fn() === window.fn() 你不传参, this ---> window
  • function.call可以指定this,但是会被篡改成对象
  • use strict可以确保通过call传入的参数不被转成对象
// function中,this默认是指向window
function fn1(){
	console.log(this)
}
fn1.call()
fn1.call(undefined)
// call可以指定this
function fn1(){
	console.log(this)
}
fn1.call(1) // Number {1}  
// JS会自动把数字1转成Number对象1
// 注意数字类型和对象类型中的Number对象不是一回事 
// use strict之后可以确保传入的参数不被转成对象
function fn1(){
	'use strict'
	console.log(this)
}
fn1.call(1) // 1

call

// 第一个参数是this
// 后面的参数放入arguments
function fn1(){
	console.log(this)
    console.log(arguments)
}
fn1.call(1,2,3,4)

this

  • this其实是为了解决一个问题:你还未创建的对象如何能在构造函数中预先被引用到?
  • JS允许你在任何一个函数中可以用this获取还未创建出来的对象的引用
  • this多数情况默认指向 点 前调用它的那个对象
    • fn() === window.fn() 因此this ---> window
  • new fn() 调用中,fn 里的 this 指向新生成的对象,这是 new 的功能之一
  • 在 fn() 调用中, this 默认指向 window,这是浏览器决定的
  • 在声明一个function还未执行该function时,我们不用去猜测里面的this指的是什么

demo1

// 如果没有this,函数很难获取对象的引用
let person={
	name:'frank',
    sayHi(){
    	console.log(person.name); // 就只能这么写了,而且如果对象名变了,这里也要跟着变
	}
}
person.sayHi();
// 现在有了this,我们就可以让函数获取对象的引用
let person={
	name:'frank',
    sayHi(){
    	console.log(this.name);
	}
}
person.sayHi(/*person会被隐式地传入*/);
===>
person.sayHi.call(person); // 大师级调用,这样的调用才能让你用的时候清清楚楚

demo2

class Person{
	constructor(name){
		this.name=name;
        // 这里的this是constructor规定我们这么写的
	}
	sayHi(){
    	console.log(/*这里你还不知道将要创建的对象名你该怎么写呢?*/)
	}
}
// 现在有了this,我们就可以让函数获取对象的引用
class Person{
	constructor(name){
		this.name=name;
        // 这里的this是constructor规定我们这么写的
	}
	sayHi(){
    	console.log(this.name)
	}
}
let person=new Person('ryan');
person.sayHi.call(person); // 大师级调用,这样的调用才能让你用的时候清清楚楚
person.sayHi.call({name:'frank'});
// this多数情况默认指向 点 前调用它的那个对象
Array.prototype.forEach1=function(){
	console.log(this);
}
Array.prototype.forEach1(); // this-> Array.prototype
var arr=[1,2,3];
arr.forEach1(); // this-> arr

demo3

var a=function(){
	console.log(this);
}
a(); // window
// 为什么函数内的this指的是window?
// var a=...    <===> var window.a=...
// this指向的就是调用的点的前面这个对象
let a=function(){
	console.log(this);
}
a(); // window
// 为什么函数内的this指的是window?
// let a=...    不会挂载到window上去,不存在window.a
// 但是this指向的依旧是window

demo4

Array.prototype.forEach1=function(){ //在Array构造函数的原型上声明了一个公用的实例方法
	console.log(this);
	for(let i=0;i<this.length;i++){
        fn(i,this[i])
    }
}
var arr=[1,2,3];
arr.forEach1((index,item)=>{console.log(index,item);}) 
// this -> arr 这是直接调用,this就指向 点前面的调用方arr(其实是JS隐藏了把arr传进去的细节)
arr.forEach1.call(arr,(index,item)=>{console.log(index,item);}) 
// this -> arr 这是大师级调用,避免this的不明不白
Array.prototype.forEach1(); // this -> Array.prototype 这是直接调用,this就指向 点前面的调用方Array.prototype(其实是JS隐藏了把Array.prototype传进去的细节)
// 只是演示以下,这里并不能用Array.prototype.forEach1()

demo5

// 给forEach传入一个伪数组
Array.prototype.forEach.call( {0:'a',length:1} , (item)=>{console.log(item)} )

函数的大师级调用

  • call应该被多用
  • 这里是讨论call实际用起来的注意点
  • 如果你不要用this,你就如下写法
function add(a,b){
	return a+b
}
add.call(undefined,1,2) // this位拿undefined/null占位

函数调用时,.call可以加深对于this的理解

  • 普通函数调用的形式,其实是JS自动给我们隐形指定了this的指向
  • 但是如果用函数.call的形式,则我们可以显性指定this的指向
  • apply与call一样的,只是参数是数组
// 以下只能作为一种理解,并不代表真实性
// 我们把一般性的函数调用方式理解为JS会为我们隐藏传递了第一个参数,this
fn(p1,p2) 理解为 fn([window,]p1,p2)
fn(1,2) 理解为 fn([window,]1,2)
obj.fn(1,2) 理解为 fn([obj,]1,2)
// 那么现实情况,如果我们想要让this指向明确,必须用.call(),虽然你不写后面那种方式也完全可以让函数拿到正确的this
// 但写了会更容易理解
fn(p1,p2) 改写为 fn.call(window,p1,p2) 或者 fn.call(undefined,p1,p2)
fn(1,2) 改写为 fn.call(window,1,2) 或者 fn.call(undefined,1,2)
obj.fn(1,2) 改写为 obj.fn.call(obj,1,2)

bind 绑定

  • bind可以绑定this
  • bind也可以绑定剩余的参数
  • 绑定完就生成一个新的函数,不会立即执行,需要你自己调用这个新的函数去执行
var fn=function(p1,p2){
	console.log(this,p1,p2)
}
fn(1,2) // this -> window
fn1=fn.bind({name:'ryan'}) // 创建了新的函数fn1,将一个对象绑定给fn,从而改变了this的指向
fn1() // this -> {name:'ryan'}
// fn1() <===> fn.call({name:'ryan'})