梳理前端知识——this指针/闭包/作用域

一 、 作用域

执行环境

执行上下文(execution context),也称执行环境,定义了变量和函数有权访问的数据。环境中的所有变量和函数都保存在一个变量对象中。当某个执行环境中的代码执行完毕后,该执行环境被销毁,保存在其中的所有变量和函数定义也随之销毁。
全局执行环境是指最外围的执行环境,在Web浏览器中,就是指window对象,所有的全局对象和函数都是作为window对象的属性和方法创建的。
每个函数都有自己的执行环境,当函数执行完毕时,函数内的变量会被销毁。
例如

function printColor(){
  var color = "blue";
  console.log(color); //blue
}
console.log(color);   //报错 "ReferenceError: color is not defined

变量color在函数printColor中建立,当函数执行完毕时,该函数的执行环境被销毁,color这个变量也被销毁,因此在全局访问是访问不到的。

没有块作用域

作用域:顾名思义,就是指变量和函数的作用范围;
块作用域, 块作用域是指用大括号{}括起来的代码会形成一个作用域,但在JavaScript中是不存在块作用域的。
例如,在C语言中:

for(int i = 1; i<5; i++){
  printf("%d",i);
}
printf("%d",i);//会报错i没有定义

但在JavaScript中:

for(var i = 1; i<5; i++){
  console.log(i);
}
console.log(i);//不会报错,且会打印出来 5;

这是因为在C语言中,大括号{}括住的部分形成了自己的作用域,在其中声明的变量i是无法在外部访问到的。而在javascript当中,没有块作用域的概念,在大括号内声明的变量,在大括号外也是可以访问的。

思考:这里的 i 在什么域里?这里会不会发生变量提升?

作用域链

在JavaScript中有作用域链的概念,作用域链其实就是执行环境的栈,在标识符解析的过程中,会沿着作用域链一层一层向上找。
程序在执行过程中,没进入一个新的执行环境就会将该执行环境压入执行环境栈中,每执行完毕跳出该执行环境,就会将执行环境弹出栈。
例如,以下这段程序,程序摘自《Javascript高级程序设计》:

var color = "blue";

function changeColor(){
  var anotherColor = "red";
  function swapColors() {
      var tempColor = anotherColor;
      anotherColor = color;
      color = tempColor; // 这里可以访问 color、 anotherColor 和 tempColor
    }                 
    // 这里可以访问 color 和 anotherColor,但不能访问 tempColor
    swapColors();
}
// 这里只能访问 color
changeColor();

执行环境的压栈顺序如下:

全局作用域(var color;function changeColor)---->
changeColor()(var anotherColor; function swapColor)---->
swapColor()(var tempColor);

作用域链顺序如下, 摘自《Javascript高级程序设计》:

作用域链从右下角向左上角能访问的范围越来越小. swapColor中可以访问changeColor函数中的变量,反之则不可以。
注:自己动手试一试 color、 anotherColor 和 tempColor这三个变量在哪里可以访问,哪里不可以访问吧~

面试题
let a = 'global';
    
    function course() {
        let b = 'zhaowa';

        session();
        function session() {
            let c = 'session';
            
            teacher();
            // 2. 函数提升
            function teacher() {
                // 3. 变量提升
                // var d = 'yy';
                // d = 'yy';
                let d = 'yy';

                console.log(d);
                // 1. 作用域链向上查找
                console.log('test1', b);
            }
        }
    }

    course();

    // *************************************
    // 4. 提升优先级 => 函数需要变量
    // 提升维度:变量优先
    // 执行维度:函数先打印

    // **************************************
    // 前置结论:函数是天然隔离的方案
    // 5. 块级作用域
    if (true) {
        let e = 111;
        var f = 222;
        console.log(e, f);
    }
    console.log('f', f);
    console.log('e', e);

二 、 闭包

什么是闭包

首先你要知道,闭包没什么神奇的,也没什么可怕的!

闭包是指有权访问另一个函数作用域中的变量的函数, 创建闭包的常见形式是在一个函数内部创建并返回另一个函数。

在这个例子中,返回的匿名函数就是一个闭包。
根据前面说的执行环境和作用域链,我们知道一个函数创建了一个执行上下文,函数内部的变量在函数外是访问不到的,get函数的a变量在外部是访问不到的,而闭包的作用就是使我们能够在函数外边访问到函数内部的变量,返回的匿名函数能够访问到get函数的内部变量a
而根据作用域链知道,一个外层函数的内部函数是可以向上搜索从而访问到外层函数的变量的,就如同get函数中的匿名函数是可以访问到get函数中的变量的,因此前者就成为了后者的闭包。而这种函数嵌套是我们最常见的闭包形式。

闭包的作用域链

仍以getA()这段代码为例,当函数get()执行完毕时,其活动对象不会被销毁,因为匿名函数的作用域链仍在引用这个活动对象(引用为0时才可以被销毁,后面内存管理会说)。
get()函数执行完毕之后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中,直到匿名函数被销毁后,get()函数的活动对象才会被销毁。因此需要手动销毁来避免内存泄漏。

var getA = get();
var res = getA();
getA = null;

由于闭包会携带父函数的作用域,因此会比其他函数占用更多的内存,从而影响性能,因此要慎重使用闭包。

闭包的副作用

作用域链的这种机制导致了闭包保存的是父函数变量的最终值。这在出现循环的时候就会产生意想不到的结果。

function createFunctions(){
  var result = new Array();
  for (var i=0; i < 10; i++){
    result[i] = function(){
      return i;
    };
  }
  return result;
}

我们希望这段代码返回一个函数数组,其中每个函数都能返回对应的索引值,但实际上由于闭包最终保存的是父函数最终的变量对象,此时的i值为10,因此最终返回值都为10。
解决方案如下:

function createFunctions(){
  var result = new Array();
  for (var i=0; i < 10; i++){
        result[i] = (function(num){
          return function(){
            return num;
          };
        })(i);
    }
    return result;
}

在这种解决方案中,调用每个匿名函数的时候,传入变量i,由于函数参数是按值传递的,所以会将变量i的当前值复制给参数num。而在这个匿名函数内部,又创建并返回了一个访问num的闭包,这样一来,result数组中的每个函数都有自己的num变量的一个副本,因此就可以返回不同的数值了。

三、this 上下文 context

  • 我家门前有条河,门前河上有座桥,河里有群鸭。
  • 我家门前有条河,'这河上'有座桥,‘这河里’有群鸭。
  • this是在执行时动态读取上下文决定的
    考察重点 - 各使用态中的指针指向
函数直接调用 - this指向是window
 function foo() {
        console.log('函数内部this', this);
    }

    foo()
隐式绑定 - this的指向是调用堆栈的上一级
 function fn() {
        console.log('隐式绑定', this.a)
    }

    const obj = {
        a: 1,
        fn
    }

    obj.fn = fn;
    obj.fn();
面试题
 const foo = {
        bar: 10,
        fn: function() {
            console.log(this.bar);
            console.log(this);
        }
    }

    // 取出
    let fn1 = foo.fn;
    fn1(); // ??
 const o1 = {
        text: 'o1',
        fn: function() {
            // 直接使用上下文 - 传统分活
            // console.log('o1fn_this', this);
            return this.text;
        }
    }

    const o2 = {
        text: 'o2',
        fn: function() {
            // 呼叫领导执行 - 部门协作
            return o1.fn();
        }
    }

    const o3 = {
        text: 'o3',
        fn: function() {
            // 直接内部构造 - 公共人
            let fn = o1.fn;
            return fn();
        }
    }

    console.log('o1fn', o1.fn()); // ?
    console.log('o2fn', o2.fn()); // ?
    console.log('o3fn', o3.fn()); // ?
  1. 在执行函数时,函数背上一级调用,上下文指向上一级
  2. 直接变成了公共函数的话,指向window
如何console.log('o2fn', o2.fn())的结果是o2
    // 1. 显式人为干涉
    fn.call(o2)
    // 2. 不许改变this
    const o1 = {
        text: 'o1',
        fn: function() {
            return this.text;
        }
    }

    const o2 = {
        text: 'o2',
        fn: o1.fn
    }
    // this指向的最后调用他的对象,执行fn时,o1.fn抢过来挂载在自己的o2fn上即可
显式绑定(bind | apply | call)
function foo() {
        console.log('函数this', this);
    }
    foo();

    foo.call({a: 1});
    foo.apply({a: 1});

    const bindFoo = foo.bind({a: 1});
    bindFoo();
追问:call、apply、bind区别
  1. call < = > apply 传参不同 依次传入/数组传入
  2. bind 返回值不同
  • 面试:手写apply & bind
    // 1. 需求:手写bind => bind位置 => Function.prototype => 原型
    Function.prototype.newBind = function() {
        // 2. bind改变原理
        const _this = this;
        const args = Array.prototype.slice.call(arguments);
        const newThis = args.shift();

        // 核心封装函数不执行
        return function() {
            // 执行核心apply
            return _this.newApply(newThis, args);
        }
    }

    // 2. 内层实现
    Function.prototype.newApply = function(context) {
        if (typeof this !== 'function') {
            throw new TypeError('Error');
        }

        // 参数兜底
        context = context || window;

        // 临时挂载执行函数
        context.fn = this;

        let result = arguments[1]
            ? context.fn(...arguments[1])
            : context.fn();

        delete context.fn;
        return result;
    }
new
    class Course {
        constructor(name) {
            this.name = name;
            console.log('this', this);
        }
        test() {
            console.log('this1', this);
        }
        asyncTest() {
            // 异步队列 => async queue
            setTimeout(function() {
                console.log('this_async', this);
            }, 100)
        }
    }

    const course = new Course('this');
    course.test();
    course.asyncTest();
思考:如何不写bind实现bind效果

es6箭头函数

var obj = {
    a:1,
    func1: function(){
        console.log(this.a)
    },
    func2: () => {
        console.log(this.a)
    }
}

obj.func1 = obj.func1.bind(obj) // 手动指定 func1 的 this 指向
var func1 = obj.func1
func1() // 1

var func2 = obj.func2
func2() // 箭头函数 this是静态绑定,this 指向 window

作业

  1. 如下代码会报错吗?如果报错请说明原因,如果不报错请说明运行结果和原因
for(var i = 1; i<5; i++){
   console.log(i);
}
console.log(i);
  1. 如下代码输出是什么?为什么?请写出js解释器实际执行的等效代码
var v='Hello World'; 
(function(){ 
   console.log(v); 
   var v='I love you'; 
})() 
  1. 如下代码输出是什么?为什么?请写出js解释器实际执行的等效代码
function main(){ 
   console.log(foo);     // ?
   var foo = 10;
   console.log(foo);     // ?
   function foo(){ 
      console.log("我来自 foo"); 
   } 
   console.log(foo);     // ?
} 
main(); 
  1. 如下代码输出是什么?为什么?
var a = 10;
var foo = {
   a: 20,
   bar: function () {
      var a = 30;
      return this.a;
   }
};

console.log(
   foo.bar(),             // ?
   (foo.bar)(),           // ?
   (foo.bar = foo.bar)(), // ?
   (foo.bar, foo.bar)()   // ?
   );
  1. 如下代码输出是什么?为什么?请写出js解释器实际执行的等效代码
var a = 10;
function main(){
   console.log(a);        // ?
   var a = 20;
   console.log(a);        // ?
   (function(){
      console.log(a);     // ?
      var a = 30;
      console.log(a);     // ?
   })()
   console.log(a);        // ?
}
main()
  1. 为什么点击所有的button打印出来的都是5而非0,1,2,3,4?要怎么修改?
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">

<title>JS Bin</title>
<script src="https://code.jquery.com/jquery-3.1.0.js"></script>
</head>
<body>
<ul>
<li><button>0</button></li>
<li><button>1</button></li>
<li><button>2</button></li>
<li><button>3</button></li>
<li><button>4</button></li>
</ul>
</body>
</html>
var buttons = $("button")

for(var i=0;i<buttons.length;i++){
   buttons[i].onclick = function(){
      console.log(i)
   }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容