JavaScript函数(下)和this

273 阅读13分钟

callStack调用栈

定义

调用栈其实就是一种解析器去处理程序的机制,它是栈数据结构。当执行环境中调用了多个函数函数时,通过这种机制,我们能够追踪到哪个函数正在执行,执行的函数体中又调用了哪个函数。

原理

JS引擎在调用一个函数前,需要把函数所在的环境push到调用栈,等函数执行完了,就会被环境pop弹出,然后return到之前的环境,继续执行后续的代码。它能追踪子程序的运行状态。

  • 每调用一个函数,解释器就会把该函数添加进调用栈并开始执行。
  • 正在调用栈中执行的函数还调用了其它函数,那么新函数也将会被添加进调用栈,一旦这个函数被调用,便会立即执行。
  • 当前函数执行完毕后,解释器将其清出调用栈,继续执行当前执行环境下的剩余的代码。
  • 当分配的调用栈空间被占满时,会引发“堆栈溢出”错误。

实例

  • 代码
 function boo (a) {
    return a * 3
  }
  function foo (b) {
    return boo(4) * 2
  }
  console.log(foo(3))
  • 分析代码
  1. 首先调用console.log(foo(3)),形成栈帧,放置于调用栈底部,也称为压栈,记录console.log(foo(3))函数执行后返回的位置。
  2. 然后调用foo(3),形成栈帧,放置于console.log(foo(3))之上,也称为压栈,记录foo(3)函数执行后返回的位置。
  3. 接着调用boo(4),形成栈帧,放置于foo(3)之上,也称为压栈,记录foo(4)函数执行后返回的位置。
  4. 当执行完boo(4)时候,返回值给foo函数之后,boo(4)被推出调用栈,也叫弹栈,返回原来的位置。foo函数继续执行,然后foo函数执行完,被推出调用栈,返回值给console.log(foo(3))函数,console.log得到foo函数的返回值,运行,输出结果,最后console.log也被推出调用栈,该段程序执行完成。

factorial阶乘函数

定义

假设n为数字,阶乘是n前面所有数的乘积(包括n)。当n1时,阶乘为1,当n不为1时,阶乘为n x (n-1)

代码实例

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

recursion递归函数

定义

本质是一种函数调用自身的操作,递归被用于处理包含有更小的子问题的一类问题。一个递归函数可以接受两个输入参数:一个最终状态(终止递归)或一个递归状态(继续递归)。

代码实例

先递进,再回归

f(4)
= 4 * f(3)
= 4 * (3 * f(2))
= 4 * (3 * (2 * f(1)))
= 4 * (3 * (2 * (1)))
= 4 * (3 * (2))
= 4 * (6)
= 24

递归函数的调用栈

递归函数的调用栈很长,下面是阶乘4的调用栈,一共有4次压栈,4次弹栈。

测试调用栈最长有多少

function computeMaxCallStackSize(){
    try{
        return 1 + computeMaxCallStackSize();
    } catch(e){
        // 报错说明爆栈了,stack overflow
        return 1
    }
}
* Chrome 12578
* Firefox 26773
* Node 12536

callstack Overflow爆栈

如果调用栈中压入的帧过多,程序就会奔溃

function Hoisting函数提升

定义

不管把具名函数放在哪里,它都会跑到语句中的第一行

代码实例

在这里并不会报错,因add函数会跑到第一行,在add(1,2)的前面,申明的具名函数会提升,称为函数提升

add(1,2)
function add(x,y){
    return x + y
}

反面实例1-报错的函数提升

因为let只准声明一次,所以add为变量数字,不能在被赋值,也不会被提升

let add = 1
function add(){} // 报错,因为add已经被申明了

反面实例2-变量提升

因为var可以被重复申明,同时var是全局变量,申明的时候会被提升,成为变量提升

function add(x,y){}
var add = 1

反面实例3-不是函数提升

因为左边的是赋值,右边的匿名函数申明不会提升,不能在函数未被申明的时候,调用函数,结果是会报错

add(1,2)
let add = function(x,y){return x + y}

arguments

arguments定义

由于JavaScript允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是arguments对象的由来。arguments对象包含了函数运行时的所有参数。 arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。

let f = function (one) {
  console.log(arguments[0]);
  console.log(arguments[1]);
  console.log(arguments[2]);
}

f(1, 2, 3)
// 1
// 2
// 3

arguments是array-lik object伪数组

需要注意的是,虽然arguments很像数组,但它是一个对象,也叫做伪数组,拥有length属性,没有数组共有的属性,比如slice()forEach(),不能在arguments对象上直接使用。

传递实际参数给arguments

在下面的代码中,fn(1,2,3)传递了三个实际参数给arguments,那么就是[1,2,3]的伪数组

function fn(){
    console.log(arguments);
}
fn(1,2,3)

this定义

面向对象语言中 this 表示当前对象的一个引用,但在 JavaScriptthis 不是固定不变的,它会随着执行环境的改变而改变

不同情况下的this指向

this指向window

不给任何条件,this默认指向window,window 就是该全局对象为[object Window]

function fn(){
    console.log(this)
}

fn()
// winow object

this为undefined

在严格模式下,如果this没有被execution context执行环境定义,那它将保持为undefined

function f2(){
  "use strict"; // 这里是严格模式
  return this;
}

f2() === undefined; // true

this指向所属对象

如果要想把this的值从一个环境传到另一个,就要用call() 或者apply()方法。

// 将一个对象作为call和apply的第一个参数,this会被绑定到这个对象。
var obj = {a: 'Custom'};

// 这个属性是在global对象定义的。
var a = 'Global';

function whatsThis(arg) {
  return this.a;  // this的值取决于函数的调用方式
}

whatsThis();          // 'Global' 指向全局对象
whatsThis.call(obj);  // 'Custom'  指向所属对象
whatsThis.apply(obj); // 'Custom'  指向所属对象

this绑定特定对象

当一个函数在其主体中使用this 关键字时,可以通过使用函数继承自Function.prototypecall()apply()方法将this 值绑定到调用中的特定对象。

function add(c, d) {
  return this.a + this.b + c + d;
}

var o = {a: 1, b: 3};

// 第一个参数是作为‘this’使用的对象
// 后续参数作为参数传递给函数调用
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16

// 第一个参数也是作为‘this’使用的对象
// 第二个参数是一个数组,数组里的元素用作函数调用中的参数
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34

假设没有this

代码实例一:引用变量

let person = { 
    name: 'frank',
    sayHi(){
        console.log(`你好,我叫`+ person.name)
    }
}
person.sayHi()

这种方法称为引用,通过对象地址的变量来获取name

代码实例一的问题

let sayHi = function(){
    console.log(`你好,我叫`+ person.name)
}
let person = {
    name:‘frank,
    'sayHi':sayHi
}
person.sayHi === ???

在上面的代码里面person如果改名,sayHi函数就挂了。同时,sayHi函数可能在另外一个文件里面,出现person指定不清楚的问题

代码实例二:使用class的问题

class Person{
    constructor(name){
        this.name = name
        // 这里的this是new强制指定的
    }
    sayHi(){
        console.log(???)
    }
}

在上面的代码中,只有类,还没有创建对象,也不可能得到对象的name。存在逻辑上的矛盾,不能对未生成的事物,进行操作

代码实例二:使用class的解答

  • 通过用arguments传给对象,使用对象的name
let person = {
    name: 'frank',
    sayHi(p){
        console.log(`你好,我叫` + p.name)
    }
}
person.sayHi(person)
  • 通过用arguments传给类,使用类的name
class Person{
    constructor(name){
        this.name = name
    }
    sayHi(p){
        console.log(`你好,我叫`+p.name)
    }
}

代码实例三:python的解答

怎么样让类,对还没有出现的实例进行操作呢? python的思路是新建对象person,通过赋值属性给这个新的person, 属性得到了保存,然后再从新对象这里调用。这样就完成立对未出现对象的操作。打个比方说,你的孩子还没出生,但是你有东西想给他,然后你把东西放在一个地方,等他出生了就可以给他了。在这里,孩子就是新的实例person,这个地方就是self

class Person:
  def_init_(self, name): # 构造函数
    self.name = name
  
  def sayHi(self):
    print('Hi,I am' + self.name)
  
  person = Person('frank')
  person.sayHi()
  1. 每个函数都接受一个额外的self,这个self就是传进来的对象,等于person
  2. 只不过Python会偷偷的把你传递对象,person.sayHi() === person.sayHi(person)。这样,person就传给self

this的出现在JS中

用this获取未出现的对象

let person = {
    name: 'frank',
    sayHi(){
        console.log(`你好,我叫` + this.name)
    }
}

在这里,person.sayHi()相对于person.sayHi(person),然后person被传给this(person是个地址)。这样,每个函数都能用this获取一个未知对象的引用了。person.sayHi()会隐式的把person作为this传给sayHi,这样做就方便sayHi获取person对应的对象

小结

  • 我们想让函数获取对象的引用,但是并不想通过变量名做到
  • python通过额外的self参数做到
  • javaScript通过额外的this做到,this就是最终调取sayHi()的对象。

this的调用方法

  • 第一种,person.sayHi()会自动把person传到this
  • 第二种,person.sayHi.call(person),需要手动把person传递函数里,作为this
  • 推荐第二种,深入学习运用,理解概念

call()方法

定义

call()方法可以用来在一个对象调用另一个对象的方法,也可以改变调用方法this的指向

function a(){
	console.log(this);
}
a();   // this 默认指向window
a.call({name:"西瓜"}); // this指向传入的对象{name:"西瓜"}

语法

function.call(thisArg,arg1,arg2,...)

原理

手动来实现一个call()方法

Function.prototype.MyCall = function(obj){
	var newObj = obj || window;
	newObj.fn = this;
	var params = [...arguments].slice(1);
	var result = newObj.fn(...params);
	delete newObj.fn;
	return result;
}
  1. 首先定义一个新的对象,若传入对象的obj存在,则新对象等于obj,若obj不存在,则等于window
  2. 然后把this挂在到当前定义的新对象上(this即为调用的函数)
  3. 4行代码得到了函数附带的参数
  4. 然后执行创建的新对象newObjfn函数
  5. 最后在执行了以后,把这个挂载的fn函数删除
  6. 返回结果result

问题

function test(){
	console.log(this);
}
test();
test.MyCall({name:"西瓜"});

所以在这里 结果为

window{****}
{name:"西瓜"}

进阶问题

function f1(a){
	console.log(1);
	console.log(this);
}
function f2(){
	console.log(2);
	console.log(this);
}

f1.call(f2);
f1.call.call(f2);

答案为:

1
f2(){console.log(2);console.log(this);}

2
window{*****}

第二个结果返回为window对象是因为在这里,可以最终简化为f2.call(),没有传入对象,所以指向window

var newObj = f2;
f2.fn = Function.prototype.MyCall;
  • this还是指向f2()
  • f1.call()就是Function.prototype.MyCall
  • 最终就是f2()调用call()得出结果
  • f1.call.call(f2) === Function.prototype.call(f2) === f2.call()

apply()方法

call(),apply()方法区别是,从第二个参数起,call()方法参数将依次传递给借用的方法作参数,而apply()直接将这些参数放到一个数组中再传递, 最后借用方法的参数列表是一样的。

语法

function.apply(this,[argumentsArray])

手动来实现一个apply()方法

Function.prototype.newApply = function(context, parameter) {
  if (typeof context === 'object') {
    context = context || window
  } else {
    context = Object.create(null)
  }
  let fn = Symbol()
  context[fn] = this
  context[fn](parameter);
  delete context[fn]
}

应用:

let array = ['a', 'b'];
let elements = [0, 1, 2];
array.push.apply(array, elements);
console.info(array); // ["a", "b", 0, 1, 2]

bind()方法

bind()也是函数的方法,作用也是改变this执行,同时也是能传多个参数。与callapply不同的是bind方法不会立即执行,而是返回一个改变上下文this指向后的函数,原函数并没有被改变

bind() 方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入bind()方法的第一个参数作为 this,第二个以及以后的参数,加上绑定函数运行时本身的参数,按照顺序作为原函数的参数来调用原函数。

语法

function.bind(this,arg1,arg2,arg3,...)

手动实现bind()方法

Function.prototype.bind = function (context,...innerArgs) {
  var me = this
  return function (...finnalyArgs) {
    return me.call(context,...innerArgs,...finnalyArgs)
  }
}

应用:

let person = {
  name: 'Abiel'
}
function sayHi(age,sex) {
  console.log(this.name, age, sex);
}
let personSayHi = sayHi.bind(person, 25)
personSayHi('男')

实例

  • 没有用到this
function add(x,y){
    return x + y
}
add.call(undefined,1,2)
// 3

上面的代码种,为什么要多写一个undefined是因为代码没有传入对象,用undefined或者null都可以

  • 使用到this
let array = [1,2,3]
Array.prototype.forEach2 = function(fn){
    for(let i = 0; i < this.length; i++){
        fn(this[i],i,this)
    }
}
array.forEach2.call(array,(item)=>console.log(item))

this是什么? 这里的this指的的是array

this 一定是数组吗? 不一定,也可以是对象,例如array.forEach2.call({0:'a',1:'b'},(item)=>console.log(item))

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])

bind() 绑定this

  1. 使用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'})
  1. 使用bind()还可以绑定其他参数
let f3 = f1.bind({name: 'frank')}, 'hi')
f3() // 等价于f1.call({name: 'frank'}, hi)

ArrowFunction箭头函数

箭头函数里面的this就是外面的this

let fn = () => console.log(this)
// window{****}

call()方法指定this也不起作用

let fn2 = () => console.log(this)
fn.call(2)
// window{****}

没有arguments

let fn3 = () => console.log(arguments)
fn3(1,2,3)
// 报错,arguments is not defined

立即执行函数

定义

IIFE(Immediately Invoked Function Expression ) 立即调用函数表达式是一个在定义时就会立即执行的JavaScript函数

(function () {
    statements
})();
  • 第一部分是包围在 圆括号运算符()里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此IIFE中的变量,而且又不会污染全局作用域
  • 第二部分再一次使用()创建了一个立即执行函数表达式,JavaScript引擎到此将直接执行函数

原理

  1. ES 5时代,为了得到局部变量,必须引入一个具名函数,这样做就多余了
var a = 1;     // 申明全局变量a
function fn(){  // 申明全局函数,其中包含局部变量a
    var a = 2
}
  1. 于是,这个函数必须是匿名函数,在函数后面加个()执行这个函数
function (){
    var a = 2
    console.log(a)
}()
  1. 但是JS认为这个语法不规范,于是程序员尝试了很多方法,发现匿名函数前面加个运算符就可以解决,!, ~, (), +, -都可运行,这样就可以形成局部作用域(推荐使用!运算符)。ES 6使用{}包括代码就可以解决。
! function (){
    var a = 2
    console.log(a)
}()
// 2
// true

以下情况需要加分号;

console.log('hi') //这里需要加;分号,。(推荐使用`!`运算符)
(function (){
    var a = 2
    console.log(a)
}())

因为console.log返回undefined,不加分号,下面的匿名函数就会往上顶,和上一句合并,变为console.log(undefined(function(){**})

更多信息

浅析javascript调用栈

JavaScript 中的 Hoisting

call stack 调用栈

你不知道的 JS 错误和调用栈常识

网道 JS函数

javascript call方法的用处及原理

如何在 JavaScript 中使用 apply(💅),call(📞),bind(➰)

call、apply和bind的原生实现

IIFE 立即调用函数表达式 MDN