变量对象、执行上下文、作用域和this大集合

362 阅读23分钟

跟作用域相关的地方:

  • 变量赋值
  • 函数执行
  • let
  • const

先看2道题:

var a = 100
function invoke(){
    var a = 200
    return function (){
        console.log(a) // 200
        console.log(this.a) //100 ,this指向window
    }
}

var f=invoke()
f()
this.a和a为什么输出不一样
var a = 100
function invoke(fn){
    var a = 200
    fn()
}

function fn(){
    console.log(a) // 100
    console.log(this.a) //100 ,this指向window
}
invoke(fn)

this.a和a为什么输出100

肯定很好奇上面例子的结果的原由,下面逐步分析

每个函数上下文,都要有这三个重要属性:

  1. 变量对象(Variable object, VO)
  2. 作用域链(Scope chain)
  3. this

变量对象

1.什么是变量对象

变量对象是与执行上下文的相关的数据作用域,存储了在上下文中定义的变量和函数声明。因为不同执行上下文的变量对象略有不同,所以变量对象一般分为全局上下文下的变量对象和函数上下文下的变量对象。

2.变量对象(VO)的创建过程

变量对象的创建,属于执行上下文中的创建阶段,依次经过以下三个过程:

创建执行上下文有两个阶段:一个是创建阶段,一个是执行阶段。变量对象的创建,属于执行上下文中的创建阶段,会依次经过三个过程:

  1. 为函数的形参赋值(函数上下文)

在进入函数执行上下文时,会首先检查实参个数,接着对实参对象和形参进行赋值。如果没有实参,属性值设为undefined;当传入的实参数量小于形参数量,则会将没有被赋值的形参赋值为 undefined

  1. 函数声明

遇到同名的函数时,后面函数会覆盖前面的函数。

  1. 变量声明

检查当前环境中通过变量声明(var)并赋值为undefined(变量提升产生的原因)

从JS引擎的角度理一理上述三个过程:

  • 函数提升和变量提升,是全局执行上下文做的准备工作;
  • 当执行函数时,又会创建一个执行上下文,做的是这个函数内部的准备工作;这就是JS引擎分析代码的"预编译阶段",做完该工作才进入执行阶段。

执行上下文的第二个阶段为执行阶段,此时会进行变量赋值,执行其他代码等工作,此时,变量对象变为活动对象(Active object, AO)。

活动对象和变量对象其实是同个东西,只是规范概念上的差异。只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫活动对象。而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。

所以明确,活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。

console.log(fn); // ƒ fn() { console.log('后声明的');}
console.log(b); // undefined
function fn() {
    console.log('先声明的');
}
function fn() {
  console.log('后声明的');
}
var b = 10;
console.log(b); // 10
var fn = 20;        
console.log(fn);//20
// 创建过程
EC= {
  VO:{}, // 创建变量对象
  scopeChain: [{VO}], // 作用域链
  this: window // this绑定
}
VO = {
  // argument: {}, // 当前为全局上下文,不存在arguments
  fn: reference to function fn(){}, // 函数fn的引用地址
  b: undefiend  // 变量提升
}

根据变量对象创建的三个过程,

首先是arguments对象的创建(全局上下文没有则忽略)
其次,是检查函数的声明。此时,函数fn声明了两次,则后一次的声明会覆盖上一次的声明。
最后,是检查变量的声明,先声明了变量b,将它赋值为 undefined;接着遇到fn的变量声明,由于fn已经被声明为一个函数,故忽略该变量声明。

到此,变量对象的创建阶段完成,接下来进行执行阶段:

1.执行console.log(fn);此时fn为声明的第二个函数,故输出结果:"后声明的"2.执行console.log(b),此时b已被赋值为undefined,故输出结果:"undefined"3.执行赋值操作: b = 10;
4.执行console.log(b) ,故输出b为105.执行赋值操作: fn = 20;
6.执行console.log(fn) ,故输出fn为20

执行到最后一步时,执行上下文如下:

// 执行阶段
EC = {
  VO = {};
  scopeChain: {};
  this: window;
}
 // VO ---- AO
AO = {
  argument: {};
  fn: 20;
  b: 10;
}

以上,就是变量对象在代码执行前及执行后的变化。

总结

全局上下文的变量对象初始化是全局对象

函数上下文的变量对象初始化只包括 arguments 对象

在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值

在代码执行阶段,会再次修改变量对象的属性值

一、几个分析作用域的必懂含义和区别

1. VO(G) AO(XXX) EC(G) EC(XXX) GO

  • 全局变量对象:VO(G)
  • 私有变量对象:AO(XXX)
  • 全局上下文:EC(G)
  • 私有上下文:EC(XXX)
  • 全局对象:GO

2. 全局对象和全局变量对象的区别

  • 全局对象GO:是浏览器自带的存储属性和方法的堆,是一个对象
  • 全局变量对象VO:是我们自己写代码创建的变量要存储的地方;是栈内存

由此想到栈内存是干啥的?

栈内存(相当于一个小空间)的作用:

1.代码执行

2.基本数据值存储在栈内存中

堆内存作用:(本次我们用不到,只是提到栈内存时,自然提一下堆内存)

1.存储引用数据类型的值

3. 全局执行上下文中:带VAR和不带VAR的区别

“在全局执行上下文中”,带VAR和不带VAR定义值是两套不同的机制

  • 带VAR是创建一个全局变量,存放在全局变量对象VO(G)中
  • 不带VAR创建的不是变量,而是全局对象GO(global object)的一个属性
f=function (){return true;};
//给GO中设置一个属性 f = function () {return true;}

4. 私有执行上下文中:带VAR和不带VAR的区别

  • 带VAR的情况

比在函数里:var x=100;

在私有上下文的AO(FN)变量对象中声明一个x的私有变量(x是当前上下文的私有变量,和上下文以外没有必然联系)

  • 不带VAR的情况

比如在函数里: y=200;

1、浏览器发现y不是私有变量,则向其上级上下文中查找,如果上级也没有则继续查找,一直到EC(G)全局上下文为止,找到哪一级,就是哪一级的变量

2、如果找到全局也没有,则给GO(window)设置一个属性:window.y=200

5. 全局和私有上下文中带var的区别

  • 全局执行上下文中:基于VAR创建变量,会给VO(G)和GO中各自存储一份;
  • 私有执行上下文中:只在私有变量对象AO中创建了变量,没有给window添加属性;

6. 变量提升

变量提升:就是在当前上下文中,JS代码执行之前要做的事情;首先会默认把所有带VAR和FUNCTION关键字的进行声明或者定义

  • 带VAR的只是提前声明
  • 带FUNCTION的会提前声明+定义
console.log(a); //undefined
console.log(func);  //f func(){...}
var a = 10;
function func(){
    console.log(b); //undefined
    var b=20;
    console.log(b); //20
}
func();
console.log(a); //10
console.log(func);  //f func(){...}

想到函数表达式,它的提升机制呢?

7. 函数声明和函数表达式的区别

  • 函数声明在JS解析时进行函数提升,因此在同一个作用域内,不管函数声明在哪里定义,该函数都可以进行调用。

  • 函数表达式的值是在JS运行时确定,在变量提升阶段,并不会给函数赋值,所以在表达式赋值完成后,该函数才能调用。

  • 函数声明可以先调用再声明,而函数表达式必须先定义再调用

JS中原本创建的匿名函数,我们最好设置一个名字,但是这个名字在函数外面还是无法使用的,只能在本函数体中使用

var func = function anonymous() {
	// console.log(anonymous); //=>当前这个函数
};
// console.log(anonymous); //=>Uncaught ReferenceError: anonymous is not defined
func();

//================================================

var func = function func() {
	console.log(func); //=>函数名func
};
console.log(func); //=>变量func,存储的值是函数
func();

变量提升在条件判断下的处理

1、全局上下文中带VAR的

  • 不论IF条件是否成立都进行(只要不在函数里面,带VAR的都要变量提升)
  • 无论是IF还是FOR中的VAR都进行提升;
console.log(n); //=>undefined
if (1 > 1) { //=>条件不成立
	var n = 100;
}
console.log(n); //=>undefined

2、全局上下文中带FUNCTION的

  • 【IE 10及以前以及谷歌等浏览器低版本状态下】:function func(){ ... } 声明+定义都处理了
  • 【最新版本的浏览器中,机制变了】:function func; 用判断或者循环等包裹起来的函数,在变量提升阶段,不论条件是否成立,只会先声明
console.log(func); //=>undefined
<!--不管条件是否成立,提升都存在-->
if (1 === 1) {
	console.log(func); //=>函数
	function func() {
		console.log('OK');
	}
	console.log(func); //=>函数
}
console.log(func); //=>函数

注:浏览器有一个特征:做过的事情不会重新再做第二遍(例如:不会重复声明)

由var又想到let为什么没有变量提升呢?

8. let和var的区别

  • ES3 定义变量的方式: var、function
  • ES6 定义变量的方式: let、 const、 class、 import

ES6 语法相对于 ES3 来说,最大的特点就是让 JS 变得更加严谨,告别 JS 松散的特点

    1. 规范

var是ES3 定义变量;

let是ES6 定义变量

    1. 变量提升

var 存在变量提升;

let 不存在变量提升,所以变量只能在声明定义后使用

  • 3.重复声明

var 允许重复声明量;

let 是不允许重复声明

  • 4.全局对象GO

var 声明的变量即是全局变量,也相当于给GO(window)设置了一个属性,而且两者建立映射机制;

let 声明的变量仅仅是全局变量,和GO没什么关系

var n = 10;
console.log(window.n); //=>10

let n = 10;
console.log(window.n); //=>undefined

    1. 暂时性死区

let 能解决基于typeof检测一个没有被声明过的变量结果是"undefined"的暂时性死区问题;

var 不能

console.log(typeof n); //=>"undefined"


console.log(typeof n); //=>Uncaught ReferenceError: Cannot access 'n' before initialization
let n = 10;

浏览器有一个BUG(暂时性死区):
基于typeof检测一个没有被声明过的变量,并不会报错,结果是"undefined";
但是从正确角度来讲应该报错才是正常的!

    1. 块级作用域

let 在遇到除对象/函数等大括号以外的大括号(例如判断和循环)时,会形成新的块级作用域

var 没有块级作用域

目前为止我们接触的上下文(作用域)只有两种:

全局上下文 EC(G)
函数执行形成的私有上下文 EC(XX)

块级作用域:ES6提供的新的上下文(作用域)形式

除对象/函数等大括号以外,如果在其余的大括号中(例如:判断和循环)出现 LET/CONST 等,则会把当前大括号包起来的部分形成一个独立的私有上下文,基于 LET/CONST创建的变量是当前块级作用域域中的私有变量;

if (1 === 1) {
    var n = 10; //=>n是全局变量
    let m = 20; //=>m是当前大括号包起来的 私有的 块级作用域中的 私有变量
    console.log(m); //=>20
}
console.log(n); //=>10
console.log(m); //=>Uncaught ReferenceError: m is not defined


// 类似于形成两个闭包(这里叫做私有的块级作用域)
{
    let x = 10,
        y = 20;
    console.log(x, y);
}

for (let i = 0; i < 3; i++) {
    console.log(i); //=>0 1 2
}
console.log(i); //=>Uncaught ReferenceError: i is not defined

9. let 和 const 的区别

const声明的变量,不能重新指向新的值(不能修改指针的指向,但是可以改变其储存值的)

const obj = {
    name: '金色小芝麻'
};
obj = [10, 20]; //=>Uncaught TypeError: Assignment to constant variable. 
//=> 它不能更改指的是:obj这个变量不能在和其它值进行关联了,也就是不能修改const声明变量的指向

obj.name = '哈哈';
console.log(obj);//=>{name: '哈哈'};
 //=>但是可以在不改变指向的情况下,修改堆内存中的信息(这样也是把值更改了),
 //=> 所以记住:const声明的变量,不能修改它的指针指向,但是可以改变其存储值的

10. 平时不注意知识点:

  • 代码执行的行为:

    形成执行环境栈ECStack=》形成一个上下文EC=>进栈执行=》词法解析=》形参赋值=》变量提升=》代码执行=》出栈销毁(有些情况是不能移出栈的(例如:全局上下文就不能移除...)

  • 最开始肯定是先执行全局下的代码,也就是形成一个全局代码的执行环境(全局执行上下文 EC(G)), EC(G)压入栈

  • 在下一次有新的执行上下文进栈的时候,会把之前没有移除的都放到栈内存的底部,让最新要执行的在顶部执行

  • 函数内执行还有:初始化作用域链

  • 变量提升包含:

    如果是函数,开辟堆内存,生成空间地址,把函数当作字符串存进堆中;

    创建函数作用域fn[[scope]] ;

    把堆的地址AAAFFF000,当作值与 fn 变量关联在一起

  • 代码执行:把值和变量关联

二、作用域

1. 概念:当前函数的作用域 = 当前函数创建时所在的作用域

2. 全局变量和私有变量:

全局变量:在全局上下文EC(G)中的全局变量对象VO(G)中,存储的变量

私有变量:

  • 当前函数执行形成的私有上下文EC(XXX)中,声明过的变量或者函数,都会存储到AO(XXX)中;
  • 函数定义的形参

3. 作用域链查找机制

1)作用域链scopeChain:<当前EC,函数[[scope]]>;

在查找变量的时候,先找自己上下文的,如果没有,按照作用域链向上级作用域找。

2) 作用域链是在函数执行的时候形成的:

执行函数的具体步骤为:

- 形成执行环境栈ECStack
- 形成一个全局上下文EC(G)
- 形成全局变量对象VO(G)
- 如果是私有变量,创建私有上下文EC(有存放私有变量的变量对象AO)
- 进栈执行(这时会把全局上下文压缩到底部)
- 初始化作用域链 scopeChain:<当前EC,函数[[scope]]>
- 初始化THIS指向
- 形参赋值(包含初始化ARGUMENTS)
- 变量提升
- ......这里我们省略了一些(比如,开辟堆内存,生成一个空间地址;加函数作用域;把堆的地址AAAFFF000,当作值与 fn 变量关联在一起)
- 代码执行
执行完可能会出栈(也可能不出栈)

函数执行都可以是进栈出栈

3)作用域链的查找机制

在当前上下文中,代码执行的过程碰到一变量时,首先看它是否是私有的

- 如果是私有的,接下来的所有操作,都是操作自己的,和别人没有关系;
- 如果不是私有的,则按照scopeChain作用域链进行查找,在哪个上下文中找到,当前变量就是谁的
......一直找到全局上下文为止

如果找到EC(G)都找不到:
** 是获取变量值就会报错; **
** 是设置值,相当于给GO加属性 **
var a = b = 12;
//=> 相当于:var a = 12; b = 12;  只有第一个带VAR,第二个不带VAR

var a=12, b=12;
//=> 相当于var a = 12;  var b = 12; 连续创建多个变量,可以使用逗号分隔;

三、this

学习如何在JS里正确使用this就好比一场成年礼。

1. 默认绑定

开启严格模式,函数内的this指向undefinded

var改成了let 或者 const,函数内变量是不会被绑定到window上

对于匿名函数,它里面的this在非严格模式下始终指向的都是window。比如setTimeout

var a = 1;
function f(){
    var a = 2;
    console.log(this)   //window,说明指向最后调用它的对象
    function inner(){
        console.log(this.a)  //1,在inner中this指向的还是window
    }
    inner()  
}
f()
var obj1 = {
  a: 1
}
var obj2 = {
  a: 2,
  foo1: function () {
    console.log(this.a)  //2
  },
  foo2: function () {
    function inner () {
      console.log(this)  //window
      console.log(this.a)  //3
    }
    inner()  //函数内包含函数,this还是指向window
  }
}
var a = 3
obj2.foo1()   
obj2.foo2()

2. 隐式绑定

this永远指向最后调用它的那个对象;

隐式丢失其实就是被隐式绑定的函数在特定的情况下会丢失绑定对象。

有两种情况容易发生隐式丢失问题:

  1. 使用另一个变量来给函数取别名
function foo () {
  console.log(this.a)
};
var obj = { a: 1, foo };
var a = 2;
var foo2 = obj.foo;
var obj2 = { a: 3, foo2: obj.foo }

obj.foo();   //1
foo2();   //2
obj2.foo2(); //3
  1. 将函数作为参数传递时会被隐式赋值,回调函数丢失this绑定
function foo () {
  console.log(this.a)   //2
}
function doFoo (fn) {
  console.log(this)   //window
  fn()
}
var obj = { a: 1, foo }
var a = 2
doFoo(obj.foo)

function foo () {
  console.log(this.a)  // 2
}
function doFoo (fn) {
  console.log(this)   //{ a:3, doFoo: f }
  fn()
}
var obj = { a: 1, foo }
var a = 2
var obj2 = { a: 3, doFoo }

obj2.doFoo(obj.foo)  //函数作为参数传递,this绑定丢失,与调用对象无关

函数内,this严格模式指向window ,非严格模式是undefinded

3. 显式绑定

3.1 改变方式:call、apply、bind

如果call、apply、bind接收到的第一个参数是空或者null、undefined的话,则会忽略这个参数,也就是指向window

function foo () {
  console.log(this.a)
}
var a = 2
foo.call()   //2
foo.call(null)  //2
foo.call(undefined)  //2

call是会直接执行函数的,bind是返回一个新函数,可以用call和bind出题

function foo () {
  console.log(this.a)   // 1
  return function () {
    console.log(this.a)  //2
  }
}
var obj = { a: 1 }
var a = 2

foo.call(obj)()

foo()函数内的this虽然指定了是为obj,但是调用最后调用匿名函数的却是window。

可以与箭头函数做比较

加到对象里的例子

var obj = {
  a: 'obj',
  foo: function () {
    console.log('foo:', this.a)
    return function () {
      console.log('inner:', this.a)
    }
  }
}
var a = 'window'
var obj2 = { a: 'obj2' }

obj.foo()()
obj.foo.call(obj2)()
obj.foo().call(obj2)

foo:obj
inner:window

foo:obj2
inner:window

foo:obj
inner:obj2
这道题果断写出来了
比较跟函数写法的区别

3.2 显示绑定的其他办法

  • 在一个函数内使用call来显式绑定某个对象,这样无论怎样调用它,其内部的this总是指向这个对象
function foo1 () {
  console.log(this.a)
}
var a = 1
var obj = {
  a: 2
}

var foo2 = function () {
  foo1.call(obj)
}

foo2()   //2
foo2.call(window)   //2  再用call来绑定window也没有用了

forEach、map、filter 第二个参数也能够显示绑定this

  • new绑定

使用new来调用一个函数,会构造一个新对象并把这个新对象绑定到调用函数中的this。

使用new函数创建的对象和字面量形式创建出来的对象好像没什么大的区别,如果对象中有属性是函数类型的话,并且不是箭头函数,那么解法都一样。

var name = 'window'
function Person (name) {
  this.name = name
  this.foo = function () {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
var person2 = {
  name: 'person2',
  foo: function() {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
  
// 使用new来调用Person,构造了一个新对象person1并把它(person1)绑定到Person调用中的this。
var person1 = new Person('person1')
person1.foo()()
person2.foo()()

'person1'
'window'
'person2'
'window'

new绑定结合call和apply

var name = 'window'
function Person (name) {
  this.name = name
  this.foo = function () {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
} 
//构造函数形式可以转变成对象形式,对person1举例如下

var person1 = {
	name: 'person1',
	foo: function () {
		console.log(this.name)
		return function () {
			console.log(this.name)
		}
	}
}

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

person1.foo.call(person2)()
person1.foo().call(person2)

  • 箭头函数绑定

this 永远指向最后调用它的那个对象,在箭头函数中不恰当。

它里面的this是由外层作用域来决定的,且指向函数定义时的this而非执行时

箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined。

箭头函数可以有哪几种题:

  1. 字面量对象中普通函数与箭头函数的区别: 只有一层函数的题目
  2. 字面量对象中普通函数与箭头函数的区别:函数嵌套的题目
  3. 构造函数对象中普通函数和箭头函数的区别:只有一层函数的题目
  4. 构造函数对象中普通函数和箭头函数的区别:函数嵌套的题目
  5. 箭头函数结合.call的题目
var name = 'window'
var obj1 = {
  name: 'obj1',
  foo: function () {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
var obj2 = {
  name: 'obj2',
  foo: function () {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  }
}
var obj3 = {
  name: 'obj3',
  foo: () => {  //箭头函数的this由外层作用域决定,因此为window
    console.log(this.name)
    return function () {  //内层普通函数由调用者决定,调用它的是window,因此也为window
      console.log(this.name)
    }
  }
}
var obj4 = {
  name: 'obj4',
  foo: () => {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  }
}

obj1.foo()() // 'obj1' 'window'
obj2.foo()() // 'obj2' 'obj2'
obj3.foo()() // 'window' 'window'
obj4.foo()() // 'window' 'window'

构造函数对象中普通函数和箭头函数的区别:一层函数的题目

var name = 'window'
function Person (name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
  }
  this.foo2 = () => {  //函数内this由外层作用域决定,且指向函数定义时的this而非执行时
    console.log(this.name)
  }
}
var person2 = {
  name: 'person2',
  foo2: () => {
    console.log(this.name)
  }
}
var person1 = new Person('person1')
person1.foo1()
person1.foo2()
person2.foo2()

'person1'
'person1'
'window'

构造函数对象中普通函数和箭头函数的区别:函数嵌套的题目

var name = 'window'
function Person (name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
  this.foo2 = function () {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  }
  this.foo3 = () => {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
  this.foo4 = () => {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  }
}
var person1 = new Person('person1')
person1.foo1()() // 'person1' 'window'
person1.foo2()() // 'person1' 'person1'
person1.foo3()() // 'person1' 'window'
person1.foo4()() // 'person1' 'person1'

箭头函数结合.call的题目

var name = 'window'
var obj1 = {
  name: 'obj1',
  foo1: function () {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  },
  foo2: () => {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
var obj2 = {
  name: 'obj2'
}
obj1.foo1.call(obj2)() // 'obj2' 'obj2'  相当于是通过改变作用域间接改变箭头函数内this的指向。
obj1.foo1().call(obj2) // 'obj1' 'obj1'
obj1.foo2.call(obj2)() // 'window' 'window'
obj1.foo2().call(obj2) // 'window' 'obj2'

箭头函数的this无法通过bind、call、apply来直接修改,但是可以通过改变作用域中this的指向来间接修改。

注意箭头函数不能使用的场景:

  1. 使用箭头函数定义对象的方法:因为this不指向对象
let obj = {
    value: 'hannie',
    getValue: () => console.log(this.value)
}
obj.getValue() // undefined

对象中的方法返回函数,获取正确的this可以通过箭头函数获取到

let obj = {
    value: 'hannie',
    getValue: function(){
        return ()=>{
            console.log(this.value)  //hannie
        }
    },
    getValue1: function(){
        return function(){
            console.log(this.value)  //undefined
        }
    }
}
let f = obj.getValue() 
f() 
let f1 = obj.getValue1() 
f1() 

  1. 定义原型方法:因为this不指向构造函数实例对象
function Foo (value) {
    this.value = value
}
Foo.prototype.getValue = () => console.log(this.value)

const foo1 = new Foo(1)
foo1.getValue() // undefined

  1. 构造函数使用箭头函数:因为函数不能成为构造函数
const Foo = (value) => {
    this.value = value;
}
const foo1 = new Foo(1)
// 事实上直接就报错了 Uncaught TypeError: Foo is not a constructor
console.log(foo1);
  1. 当要获取当前被点击的元素,作为事件的回调函数:this指向window
const button = document.getElementById('myButton');
button.addEventListener('click', () => {
    console.log(this === window); // => true
    this.innerHTML = 'Clicked button';
});

回调函数里的this指向

四、几道经典题

  • 考察作用域
var a = 0,  
    b = 0;
function A(a) {
    A = function (b) {
        console.log('inner:',a + b++)
    }
    console.log('outer:',a++)
}
A(1)
A(2)  //执行的全局A,也就是A里面的A,a已经变成2,b是2,2+2等于4
<!--外面的A先压入栈,里面的再压入A,所以第二次执行的是里面的-->

> "outer:" 1
> "inner:" 4

仅仅执行
A(2)
> "outer:" 2
var ary = [12, 23];
function fn(ary) {
    console.log(ary);// [12,23]
    ary[0] = 100;
    ary = [100];
    ary[0] = 0;
    console.log(ary);// [0]
}
fn(ary);
console.log(ary);// [100,23]

var i = 20;
function fn() {
    i -= 2;
    var i = 10;
    return function (n) {
        console.log((++i) - n);
    }
}
var f = fn();
f(1); //10   11-1  i=10
f(2); //10   12-2  i=11
fn()(3); //8 11-3  i=10
fn()(4); //7 11-4  i=10
f(5);  //8   13-5  i=12
console.log(i);  //20

重复执行f,会保留i
//========有形参=======
let x = 5;
function fn(x) {
    return function(y) {
        console.log(y + (++x));
    }
}
let f = fn(11);
f(7); //19   x是11
fn(18)(9); //28   x是18
f(10); //23    x是12,因为f(7);执行了一次,x是局部变量,没有被fn(18)(9)覆盖
console.log(x); //5   全局的
重复执行f,会保留x

//========无形参==========
let x = 5;
function fn() {
    return function(y) {
        console.log(y + (++x));
    }
}
let f = fn(6);
f(7);//13   x是5
fn(8)(9);//16   x变成6
f(10);//18   x变成7
console.log(x);//8    x变成8

实参是局部变量

console.log(a, b, c);//=> undefined undefined undefined
var a = 12,
    b = 13,
    c = 14;
function fn(a) {
    console.log(a, b, c);// 10 , 13 , 14
    a = 100;
    c = 200;
    console.log(a, b, c);// 100 , 13 ,200
}
b = fn(10); //函数没返回值
console.log(a, b, c);// 100 undefined 200

var a=1;
var obj ={
   "name":"tom"
}
function fn(){
   var a2 = a;
   obj2 = obj; //共用一个地址
   a2 =a;
   obj2.name =”jack”;
}
fn();
console.log(a);// 1
console.log(obj);// {"name":"jack"}

考擦函数的变量提升和浏览器的一个特征:做过的事情不会重新再做第二遍

/*
 * 全局上下文中的变量提升
 *     fn = function(){ 1 }  声明+定义
 *        = function(){ 2 }
 *     var fn; 声明这一步不处理了(已经声明过了)
 *        = function(){ 4 }
 *        = function(){ 5 }
 * 结果:声明一个全局变量fn,赋的值是 function(){ 5 }
 * 注意:fn()都是执行,没有声明
 *       fn不会重复声明
 */
fn(); //5   全局变量fn,取最后面一个
function fn(){ console.log(1); }  //=>跳过(变量提升的时候搞过了)
fn(); //5
function fn(){ console.log(2); }  //=>跳过
fn(); //5

var fn = function(){ console.log(3); }  //不会再声明了,因为function fn(){}已经声明过了,但是会赋值,因为变量提升没有做过

fn(); //3  执行的是表达式函数
function fn(){ console.log(4); } //=>跳过
fn(); //3
function fn(){ console.log(5); } //=>跳过
fn(); //3

  • 考擦大括号内函数的变量提升
/*
 * 高版本浏览器 
 * 全局上下文中变量提升:没有
 */
f=function (){return true;}; 
g=function (){return false;}; //不带var,得到全局的g
(function () {
	// 条件解析g()执行的是if中的g,先在本作用域查找,如果找不到,再根据作用域链往上查找
	// g() => undefined() => Uncaught TypeError: g is not a function 下面操作都不在执行了
    if (g() && [] == ![]) {
        f = function () {return false;} 
        function g() {return true;}
    }
})();
console.log(f());
console.log(g());

var name = 'World!';
(function () {
    if (typeof name === 'undefined') {
        var name = 'Jack'; //变量提升
        console.log('Goodbye ' + name);
    } else {
        console.log('Hello ' + name);
    }
})();
> "Goodbye Jack"
 var str = 'World!';   
    (function (name) {
    if (typeof name === 'undefined') {
        var name = 'Jack';
        console.log('Goodbye ' + name);
    } else {
        console.log('Hello ' + name);
    }
    })(str);
 //Hello World 因为name已经变成函数内局部变量
  • 思考几道非匿名自执行函数的题
var b = 10;
(function b(){
    // 'use strict'
    b = 20
    console.log(b)
})()

>  [Function b]
  • 如标题一样,非匿名自执行函数,函数名不可以修改,严格模式下会TypeError,
  • 非严格模式下,不报错,修改也没有用。
  • 查找变量b时,立即执行函数会有内部作用域,会先去查找是否有b变量的声明,有的话,直接复制
  • 确实发现具名函数Function b(){} 所以就拿来做b的值
  • IIFE的函数无法进行赋值(内部机制,类似const定义的常量)
var b = 10;
(function b(){
    console.log(b) //var b的变量提升
    b = 5
    console.log(window.b) 
    var b = 20
    console.log(b)
})()


> undefined
> 10
> 20

在立即执行函数中加上var b = 20

var b = 10;
(function b(){
    console.log(window.b)
    var b = 20 // IIFE内部变量
    console.log(window.b) 
    console.log(b)
})()
> 10
> 10
> 20
// 访问b变量的时候,发现var b = 20;在当前作用域中找到了b变量,于是把b的值作为20

window.b变成window.c,为什么会是0

var b = 10;
(function c(){
    console.log(window.c) 
    console.log(b)
})()
> undefined
> 10
c= 10; //没有var,定义在GO上
function b(){
    console.log(window.c)  //
}
b()
> 10

参考链接:

juejin.cn/post/684490…

juejin.cn/post/685457…

juejin.cn/post/684490…