4.函数执行作用链、闭包、this

262 阅读9分钟

JavaScript中的函数是一等公民

  • 在JavaScript中,函数是非常重要的,并且是一等功民
    • 可以作为参数传递
    • 也可以作为返回值被返回
  • 自己编写高阶函数(接收函数为参数或者返回另外一个函数)
  • 使用内置高阶函数
//作为参数被传递
function computed(num1, num2, calcFn){
    calcFn(num1,num2);
}
function add(n1,n2){
    console.log(n1+n2);
};
function mul(n1,n2){
    console.log(n1-n2);
};
computed(10,20, add); //30
computed(40,20, mul); //20

//作为返回值
function foo(fn){
    return fn;
}
function bar(){
    console.log('bar')
}
foo(bar);

数组中的函数使用

函数和方法的区别:

var obj = {
    add: function(num1, num2){
        return num1+num2;
    }
}
function add(num1, num2){
    return num1+num2;
}

第一个对象obj中add就是obj对象的方法,第二个add中的function就是作为一个独立的函数.

过滤数组得到一个新的数组,里面的元素都是偶数

var nums = [10,25,55,30,28,50];
//filter过滤
var newNums = nums.filter(item=>{
    return item%2===0;
})
//map映射
var newNums = nums.map(item=>{
    return item%2===0;
})

//foreach迭代
var newNums = nums.forEach(item=>{
    return item%2===0;
})

// find和findIndex
let friends =[
  {name: 'James', age: 35},
  {name: 'kobe', age: 25},
  {name: 'rose', age: 28},
  {name: 'Yao', age: 33},
]

let target = friends.find(item=>{
  return item.name === 'Yao';
})
console.log(target); //{name: 'Yao', age: 33}

let targetIndex = friends.findIndex(item=>{
  return item.name === 'Yao';
})
console.log(targetIndex); //3

JavaScript中的闭包的定义

闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境
闭包的概念出现于60年代,最早出现闭包的程序是Scheme,JS作者又是Scheme的热衷粉,因此JavaScript中大量的设计来源于Scheme的.
MDN闭包的解释:

  • 一个函数和对其周围状态的引用捆绑在一起,这样的组合叫闭包
  • 闭包可以让你实现一个内层函数中访问到其外层函数的作用域.

闭包的实现

function foo(){
   var name = '123';
   function bar(){
        console.log('bar', name);
    }
    return bar;
}
var fn = foo();
fn();

image.png 案例来说,foo函数执行完毕就已经被销毁,但是实际在bar函数执行完成内部依然可以访问到变量name属性,所以综上所述闭包是包含两部分组成的:函数+可以访问的自由变量(对应代码就是bar函数和name属性),词法闭包在解析的过程中就已经确定了可以访问的自由变量,而不是定义的时候确定.

闭包与函数的最大区别是:

当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即使脱离捕捉时的上下文,它也能正常执行。

var target = '123';
function test(){
    console.log('target',target);
}

狭义的闭包:JavaScript中的一个函数,如果访问到了外层作用域的变量,那么它就是一个闭包

为什么函数已经销毁了内部函数依然可以访问到它的变量呢?
foo对象表面上会被销毁,但是fn这个变量会引用bar的地址,bar的父级引用又是foo,所以foo对应的对象其实没有被销毁. 因此函数闭包会容易导致内存泄露,

闭包的内存泄露案例

function createFnArray(){
    var arr = new Array(1024*1024).fill(1); //占据4M空间 4bit*1024*1024
    return function (){
        console.log(arr.length);
    }
}

var arrFns = [];
for(let i =0; i<100; i++;){
    arrFns.push(createFnArray());  //400M
}

arrFns = null; //释放内存

对于闭包中的自由变量

function foo(){
    var name = "why";
    var age = 18;
    function bar(){
        console.log(name);
    }
    return bar;
}
var fn = foo();
fn();

对于age属性V8引擎发现一直没有被使用,所以就会被销毁.JS引擎会对其做优化 将其临时删除掉,只能访问到name属性 image.png

this的指向

var obj = {
    name: 'why',
    eat(){
        console.log(this.name+'吃东西.');
    },
    run(){
        console.log(this.name+'在跑步.');
    },
    study(){
        console.log(this.name+'在学习.')
    }
}
obj.eat();
obj.run();
obj.study();

通过上面简单的实例可以看出this的指向是obj

this的全局作用域指向

console.log(this) //Node:{} 浏览器中:global window对象

大多数中this都是出现在函数中的。在Node环境中,this会被当做成一个模块进行加载,然后编译以后放置到一个函数中,执行这个函数.call,对于Node环境来说call方法传值的第一个参数是一个空对象{}
this的难点在于this并不是解析时就已经确定的,它是运行生成的.

function foo(){
    console.log(this);
}
var obj = {
    foo:foo
}
foo(); //window
obj.foo(); //obj
foo.apply({}) // {}

this指向什么,跟函数所处的位置是没有关系的。跟函数的调用方式有关.

this的绑定规则:

  • 默认绑定
  • 隐式绑定
  • 显式绑定
  • new绑定 默认绑定
//作为独立函数调用
function callBack(){
    console.log(this);
}
callBack(); // window

function foo1(){
    console.log(this);
} 
function foo2(){
    console.log(this);
    foo1();
}
function foo3(){
    console.log(this);
    foo2();
}
foo3(); //window

var obj = {
    foo:function(){
        console.log(this);
    }
}
obj.foo(); //obj
var a = obj.foo;
a(); //window
//////////////////////////////////////
function foo(){
    console.log(this);
}
var obj = {
   foo:foo
}
var bar = obj.foo;
bar(); //window

通过上面两个例子可以得出一个简单的结论,this的结果是什么取决于谁调用它,没有调用主题的时候指向了window,有调用主题时,指向了调用主题.

隐式绑定,是指通过某个对象进行调用的 它的调用位置是通过某个对象发起的。

var obj = {
    name: 'why',
    foo:function(){
        console.log(this);
    }
}    

var fn = obj.foo;
fn(); //window
//////////////////////////////////////

var obj1 = {
    foo:function(){console.log(this);}
}
var obj2 = {
    bar: obj1.foo
}
obj2.bar();

obj对象的会被JavaScript引擎绑定到fn函数中的this里面,所以这种称之为隐式绑定

显式绑定,必须在调用对象的内部有一个对函数的引用,如果没有引用会提示错误

function sum(m,n){
    console.log(m+n,this)
}
sum.call({},20,30); //50,{}

function sum(m,n){
    console.log(m+n,this)
}
sum.apply({},[20,30]); //50,{}

//默认绑定与显式绑定发生冲突时,优先级较高的是显式绑定
function foo(){
    console.log(this);
}
var newFoo = foo.bind('aaa');
newFoo(); //this=> String{"aaa"}

callapply的区别:传参方式不同,call直接以,进行分割;apply的传参方式是以一个[]的方式.callapply的相同之处在于可以指定this是谁; 表面上看newFoo调用主题时window,但是在调用之前就已经显示绑定到String对象上的String:{'aaa'}上面了,所以当默认绑定与显式绑定发生冲突时,优先级较高的是显式绑定.

new绑定,可以把函数当做一个类的构造函数来使用,也可以使用new关键字

function Person(){console.log(this)};
let p1 = new Person(); // this=>p1
let p2 = new Person(); //this=>p2
  1. 创建一个全新的对象
  2. 新对象会被执行prototype链接
  3. 新对象会绑定到函数调用的this上

通过这种构造器constructor的形式创造出来的this指向的就是创造构造器的本身.

this的其他事例

setTimeout的内部this是什么?

function realizeSetTimeout(cb, duration){
    cb();
}
realizeSetTimeout(function(){
    console.log(this); //window
},1000)

//模拟实现setTimeout
setTimeout(function(){
    console.log(this);
},2000)

上面自己模拟实现了setTimeout中的this的实现,可以看到回调函数cb是作为独立函数被主题者调用的,所以这里的this指向的就是window,类似于内部作为独立函数执行的还有forEach,map,reduce等数组循环,可以通过修改第二个参数来更改this指向

let arr = [1,2,3];
arr.forEach(function(item){
    console.log(this); //this => window
})

arr.forEach(function(item){
    console.log(this); //this => {}
}, {})

DOM事件中的this

oDiv.onclick=function(){
    console.log(this);
}
oDiv.addEventListener('click', function(){
    console.log(this);
})

//addEventListener的模拟内部实现过程:
function addEventListener(type, cb){
    cb.call(oDiv) //这里通过call的方式把this指向了oDiv
}

所以在DOM事件中的this调用指向了调用者oDiv

this绑定规则的优先级

  • 默认(独立函数)绑定优先级最低
  • 显式绑定优先级高于隐式绑定优先级
  • new绑定高于隐式绑定
  • new绑定优先级高于bind
//显式高于隐式绑定
var obj = {
    foo:function(){
        console.log(this);
    }
}
obj.foo(); //this => obj
obj.foo.call({}); //this=>{},前面既有显式绑定又有隐式绑定,显式高于隐式

//new绑定高于隐式绑定
var obj = {
    foo: function(){
        console.log(this);
    }
}
var f = new obj.foo(); //foo

//new绑定优先级高于bind
function foo(){
    console.log(this);
}
var bar = foo.bind("aaa");
var obj = new bar();

结论: new关键字绑定 > 显式绑定(call/apply/bind) > 隐式绑定 > 独立绑定

this规则之外-忽略显示绑定

显式绑定的传值为null/undefined时:

function foo(){
    console.log(this);
}
foo.call(null); //window
foo.apply(undefined); //window

使用显式绑定时候传入null或者undefined作为第一个参数时,this默认指向window,而不是undefined或者null

间接函数引用

var obj1 = {
    foo:function(){
        console.log(this);
    }
};
var obj2 = {};
obj2.foo = obj1.foo; //this=>obj2

(obj2.foo = obj1.foo)(); //作为独立函数调用,this=> window

箭头函数的this指向

  • 箭头函数不会绑定thisarguments属性
  • 箭头函数不会作为构造函数来用
//箭头函数的简写
let foo = (item)=>{
    console.log(this);
}
//等价于:let foo = item => console.log(this);

//当返回结果是一个对象时,箭头函数的简写
let foo = ()=>{
    return {
        name: 'hello',
        age: 18
    }
}
//返回一个对象
let foo = ()=>({name: 'hello', age: 18});

箭头函数不适用于this绑定的4种规则,箭头函数的内部this指向外层作用域的this

let foo = ()=>{
    console.log(this);
}
foo();
var obj = {foo:foo};
obj.foo(); //this => window
foo.call("abc"); //this=> window

无论对箭头函数使用何种形式的绑定,最终都是会去其上层作用域进行查找. image.png

let obj = {
    data: [],
    getData:function(res){
        setTimeout(function(){
            this.data = res //报错,这里的函数作为作为独立函数执行的,this指向的是window。window的data属性为res
        })
    }
}

//修改以后:
let obj = {
    data: [],
    getData:function(res){
        setTimeout(()=>{
            this.data = res //箭头函数的this指向上层作用域 也就是obj
        })
    }
} 

this相关面试题

面试题1

var name = "window";
var person = {
  name: "person",
  sayName: function(){
    console.log(this.name);
  }
}

function sayName(){
  var sss =person.sayName;
  sss();  //独立函数调用 this => window 
  person.sayName(); //隐式调用  this => person
  (person.sayName)(); //相当于隐式调用person
  (b=person.sayName)(); //赋值给b,间接函数b引用相当于独立调用window
}

sayName();

面试题2

var name = 'window'

var person1 = {
  name: 'person1',
  foo1: function () {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name)
    }
  },
  foo4: function () {
    return () => {
      console.log(this.name)
    }
  }
}

var person2 = { name: 'person2' }

 person1.foo1(); // person1(隐式绑定)
 person1.foo1.call(person2); // person2(显示绑定优先级大于隐式绑定)

 person1.foo2(); // window(不绑定作用域,上层作用域是全局,上层作用域不是person1,是全局,这里易错)
 person1.foo2.call(person2); //箭头函数不管你是怎么调用,都不绑定this,都回去上层作用域查找,this=>window

 person1.foo3()(); // window(独立函数调用)
 person1.foo3.call(person2)(); //取到person2作为this调用,返回是一个独立函数,最终this=> window(独立函数调用)
 person1.foo3().call(person2); //显式调用, person2(最终调用返回函数式, 使用的是显示绑定)

 person1.foo4()(); // person1(箭头函数不绑定this, 上层作用域this是person1)
 person1.foo4.call(person2)(); // person2(上层作用域被显示的绑定了一个person2)
 person1.foo4().call(person2); // person1(上层找到person1)

面试题3

var name = 'window'

function Person (name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
  },
  this.foo2 = () => console.log(this.name),
  this.foo3 = function () {
    return function () {
      console.log(this.name)
    }
  },
  this.foo4 = function () {
    return () => {
      console.log(this.name)
    }
  }
}

var person1 = new Person('person1')
var person2 = new Person('person2')

person1.foo1() // person1
person1.foo1.call(person2) // person2(显示高于隐式绑定)

person1.foo2() // person1 (上层作用域中的this是person1)
person1.foo2.call(person2) // person1 (上层作用域中的this是person1)

person1.foo3()() // window(独立函数调用)
person1.foo3.call(person2)() // window
person1.foo3().call(person2) // person2

person1.foo4()() // person1
person1.foo4.call(person2)() // person2
person1.foo4().call(person2) // person1

面试题4

var name = 'window'

function Person (name) {
  this.name = name
  this.obj = {
    name: 'obj',
    foo1: function () {
      return function () {
        console.log(this.name)
      }
    },
    foo2: function () {
      return () => {
        console.log(this.name)
      }
    }
  }
}

var person1 = new Person('person1')
var person2 = new Person('person2')

person1.obj.foo1()() // window
person1.obj.foo1.call(person2)() // window
person1.obj.foo1().call(person2) // person2

person1.obj.foo2()() // obj
person1.obj.foo2.call(person2)() // person2
person1.obj.foo2().call(person2) // obj


// 

// 上层作用域的理解:对象是没有上层作用域空间的
// var obj = {
//   name: "obj",
//   foo: function() {
//     // 上层作用域是全局
//   }
// }

//上层作用域是有上层空间的.
// function Student() {
//   this.foo = function() {

//   }
// }