1. 作用域

1.1什么是作用域

javascript的每一段代码都是需要编译;编译中是如果把一段javascript代码转化成计算机语言可识别的那,这里需要讲解两个概念:

  1. 分词/词法分析:顾明思议就是把一段代码如var a = 10, 解析拆分成一段对(编译语言)有意义的代码块var、a、=、10; 空格是否会被当成词法单元,取决于空格是否被当成词法单元(取决与这个语言对空格在这门语言中是否具有意义); 一个语句的拆解就需要靠词法分析,得出结论。
  2. 解析/语法解析(AST): 这个过程就是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。(这个树被称作抽象语法树)
  3. 最后就是把解析后的结构编译成一组机器码

在一段代码执行中主要的成员在负责: 1)引擎; 2)编译器; 2) 作用域;

  1. 引擎: 从头到尾负责整个javascript程序的编译及执行
  2. 编译器: 引擎的好朋友,负责语法分析及代码生成等
  3. 作用域: 负责收集并维护所有声明的标识符(变量)_

作用域: 1) 当前作用域; 2) 全局作用域(对变量的查找LHS, RHS都是从当前作用域开始)
在作用域中查找变量有两个错误需要解释一下: ReferenceError, TypeError;
其中争对严格模式和非严格模式下有一个对比,如果在非严格模式下:如果变量没有找到,则会在全局作用域下创建一个变量,则不会报错; 但是在严格模式下: 则不允许隐式创建,则会出错,如果报ReferenceError:所有嵌套的作用域中遍历不到所需的变量、TypeError:代表作用域判断成功,但是对结果的操作是非法或者不合理的。
注: 作用域变量的查找会找到第一个匹配的标识符时停止。

1.2 函数作用域和块作用域

  1. 函数作用域: 属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。

特点: 最常用的作用域单元,生命在一个函数内部的变量或者函数会在所处的作用域中“隐藏”起来;

  1. //在函数作用域中主要提现函数作用域嵌套,变量从内层往外层依次查找变量,直到找到为止,就不会往后找
  2. function func(){
  3. var a = '我是内部变量' //声明在函数内部
  4. console.log("a",a)//这里可以整成输出
  5. }
  6. console.log("a",a);// ReferenceError 找不到变量
  1. {}以一个块级为一个单元的的作用域。
  1. //在javascript中,{}代表一个作用域,
  2. 思考问题: 这个作用域中发内部定义的变量是否具有隐蔽性
  3. function func(){
  4. //代码块
  5. {
  6. var data = 1;
  7. let data1 = 3;
  8. const data2 = 4
  9. data2 = 5; //这里会报错: const ES6属性是不可以更改的
  10. }
  11. //在这里是否可以调用;
  12. console.log("data",data); //1
  13. console.log("data",data1); //ReferenceError
  14. console.log("data",data2); //ReferenceError
  15. }

let的循环

看一下简单的for循环块级作用域

  1. //比较一下传统的
  2. function func(){
  3. for(var i = 0; i < 10; i++){
  4. if(!i){
  5. console.log("我这里肯定能拿到",i)
  6. }
  7. }
  8. //这里肯定也能拿到。 因为var基本在块级作用域中直接无需在乎了。
  9. console.log("我是外面的作用域",i); //10
  10. }
  11. //let改变了
  12. //比较一下传统的
  13. function funcLet(){
  14. for(let i = 0; i < 10; i++){
  15. if(!i){
  16. console.log("我这里肯定能拿到",i)
  17. }
  18. }
  19. //这里会报错
  20. console.log("我是外面的作用域",i); //referenceError 无法找到,
  21. }

上面代码:明显看出,在let所有的作用域中自己形成了一个封闭式的作用单元,不会被污染变量;
官方解释:
for循环头部的let不仅将i绑定到了for循环的块中,事实上它将其重新绑定到循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值

  1. {
  2. let j;
  3. for(j = 0; j < 10; j++){
  4. let i = j; //每一次迭代重新绑定
  5. console.log(i);
  6. }
  7. }

在作用域中会发生一些神奇的事情也就是我们业界所说的:“函数提升,变量提升”?具体是什么?

1.3 提升

我先讲一下原理:为什么变量声明和函数声明会被提升,是因为浏览器的编译会先去查找每一个声明的对象汇总,用于开辟内存空间或者判断声明的变量是否存在,所以此时会先执行所有的变量声明或函数声明。
这里有一个容易搞混的地方:

1.3.1 情况一:

  1. //先执行
  2. foo(); //这里可以找到foo,因为函数声明提升了,但是函数内部的变量会被提升嘛。执行看看。
  3. function foo(){
  4. console.log(a);
  5. var a = 2;
  6. }

结果: **被执行的foo内部的逻辑还是会留在原地,如果提升改变了代码的执行顺序,会造成严重的破坏

image.png

1.3.2 情况二

  1. //这里说一个概念
  2. 1. 函数声明
  3. foo(); //显然可以找到
  4. function foo(){}
  5. 2. 函数表达式
  6. foo1() //这里会报TypeError( foo1 is not a function):我想应该能猜到,由于先声明的一定是undefined
  7. bar(); //这个也不能使用,ReferenceError
  8. var foo1 = function bar(){}

1.3.3 函数优先

输出: 会输出1 而不是2!

  1. foo(); //1
  2. var foo; //这个声明会被忽略掉,被funcion foo,优先提升了
  3. function foo(){
  4. console.log("1");
  5. }
  6. foo = function(){
  7. console.log(2);
  8. }

以上代码会被引擎理解成

  1. function foo(){
  2. console.log(1);
  3. }
  4. foo(); //1
  5. var foo;
  6. foo = function (){
  7. console.log(2);
  8. }

1.4 提升的总结

  1. 1 变量或者函数的声明及使用,是执行了javascript引擎的两个阶段:<br />阶段1: 编译阶段的任务<br />阶段2: 执行阶段的任务<br />这意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程就是提升的过程。<br />2 声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会被提升

2. 作用域闭包

直接了当的定义: 当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

2.1 从全局作用域的角度分析

简单的实例

  1. function foo(){
  2. var a = 2;
  3. function bar(){
  4. console.log("a",a);
  5. }
  6. return bar;
  7. }
  8. var baz = foo(); //这里会返回一个方法;
  9. baz(); //2
  10. /**
  11. * 解析: 函数bar()的词法作用域能访问foo()的内部作用域,然后我们将bar()当成了值传递;
  12. 为什么会形成闭包?
  13. 1) 第一函数作用域内部调用了另外一个函数(函数传递),使得传递函数在执行时可以获取
  14. 执行内部的作用域的使用权限,并且会持续性使用(想什么时候用,什么时候用)
  15. 2) 从内存回收机制上分析: 由于外部函数如果被调用完毕,垃圾回收机制会根据判断是否还在使用,
  16. 如果没有使用会被注销,回收掉;但是闭包会阻止这个事情:因为内部作用域依然存在,由于内部bar、
  17. 还在使用内部的变量,bar依然持有对该作用域的引用,这个引用就叫做闭包
  18. */

经典闭包

关于for循环和setTimeout一次输出对应的值

  1. //简单的思考: 这个自然是不能输出1,2,3..5;
  2. 但是为什么??
  3. for (var i=1; i<=5; i++) {
  4. setTimeout( function timer() {
  5. console.log( i );
  6. }, i*1000 );
  7. }

这个问题就是: 延迟执行的函数会在循环执行完毕后才会一次执行;这里等于调用了5此setTimeout,但是每次都会在整个for循环执行完毕后才开始执行,此时简单的想想也明白:i是一个全局作用域的变量,最终i=6;
会循环输出5次6;
如何才能让它有自己的私有i,(因为延迟函数会晚执行,我们需要保留会记住我们需要的每个i值,不被回收),如果能真正理解这个点,应该就能很好的解决这个问题比如

  1. 粗略版本
  2. for (var i=1; i<=5; i++) {
  3. (function() {
  4. var j = i; //在一个封闭的方法内保留住值的引用
  5. setTimeout( function timer() {
  6. console.log( j );
  7. }, j*1000 );
  8. })();
  9. }
  10. 优化版本
  11. for (var i=1; i<=5; i++) {
  12. (function(j) {
  13. setTimeout( function timer() {
  14. console.log( j );
  15. }, j*1000 );
  16. })(j);
  17. }

ES6针对for,setTimeout的问题,带来了什么解决办法??

let的声明,可以用来劫持块作用域,并且在这个作用域中声明一个变量,等于说:for循环的每次操作,都等同于声明一次let i = 0; let i = 1; let i = 2; 这样就等同于在每一个块中都独立了一个变量。并且用于setTimeout中的调用.是不是很牛逼。。。。

  1. for (var i=1; i<=5; i++) {
  2. let j = i; // 是的,闭包的块作用域!
  3. setTimeout( function timer() {
  4. console.log( j );
  5. }, j*1000 );
  6. }
  7. 等同于我们常常写的
  8. for (let i=1; i<=5; i++) {
  9. setTimeout( function timer() {
  10. console.log( i );
  11. }, i*1000 );
  12. }

包的导入(原生写法)

  1. //定义一个模块导出的方法
  2. var MyModules = (function Manager(){
  3. var modules = {};
  4. function define(name, deps, impl){
  5. for(var i = 0; i<deps.length;i++){
  6. deps[i] = modules[deps[i]];
  7. }
  8. //模块对象的映射
  9. modules[name] = impl.apply(impl, deps);
  10. }
  11. function get(name){
  12. return modules[name];
  13. }
  14. return {
  15. define: define,
  16. get: get
  17. };
  18. })();

这段代码的核心是 modules[name] = impl.apply(impl, deps)。为了模块的定义引入了包装函数(可以传入任何依赖),并且将返回值,也就是模块的 API,储存在一个根据名字来管理的模块列表中。
下面展示了如何使用它来定义模块

  1. //声明一个bar的模块,有一个hello的方法,并返回了包装后的对象
  2. MyModules.define( "bar", [], function() {
  3. function hello(who) {
  4. return "Let me introduce: " + who;
  5. }
  6. return {
  7. hello: hello
  8. };
  9. } );
  10. //定义了一个foo,并且需要导入bar的模块,调用bar的方法
  11. MyModules.define( "foo", ["bar"], function(bar) {
  12. var hungry = "hippo";
  13. function awesome() {
  14. //由于当前作用域已经导入了bar,所以在整个作用域中是可以导入bar的对象
  15. console.log( bar.hello( hungry ).toUpperCase() );
  16. }
  17. return {
  18. awesome: awesome
  19. };
  20. } );
  21. //得到bar
  22. var bar = MyModules.get( "bar" );
  23. //得到foo
  24. var foo = MyModules.get( "foo" );
  25. console.log(
  26. bar.hello( "hippo" )
  27. ); // Let me introduce: hippo
  28. foo.awesome(); // LET ME INTRODUCE: HIPPO

ES6导入,导出

import,export
import 可以将一个模块中的一个或多个 API 导入到当前作用域中, export 会将当前模块的一个标识符(变量、函数)导出为公 共 API。这些操作可以在模块定义中根据需要使用任意多次。

3. this用法

this这个关键字,伴随着整个javascript的上下文,可能在目前流行的框架比如react中,也有上下文的概念,那上下文是指代什么? 我的理解是:它指代的就是这个作用域的环境, 比如:全局windows环境,块级作用域环境(等看似对象或者变量声明的静态环境);
由于javascript声明一个变量或者方法, 不通过this的方式去调用时属于一种静态环境,但是由于this这个概念,就伴随着一种动态环境,比如this.name, this.fun();看一个例子

  1. //静态环境
  2. function foo() {
  3. console.log( a ); // 2
  4. }
  5. function bar() {
  6. var a = 3;
  7. foo();
  8. }
  9. var a = 2;
  10. bar();

以上情况设想一番:
foo是在bar中调用的,如果存在动态作用域的话,在bar中又重新给a赋值,所以应该输出3,但是结果确实还是输出了2,证明这个问题并没有动态作用域,是词法作用域,
结论: 调用的还是全局变量的值,而不会根据你调用在哪里去取调用位置处变量的值

关于this的讨论

为什么所this的使用,会存在动态作用域,请看例子

  1. var count = 10;
  2. var obj = {
  3. count: 0,
  4. cool: function coolFn() {
  5. setTimeout( function timer(){
  6. //输出count的值
  7. console.log(++this.count); //11
  8. }, 100 );
  9. }
  10. }

结论: 这里会输出11,由于this的调用,实在setTimeout的作用域内,它隶属于window全局对象,所以此时的调用this指向的就是windows全局;
避免混淆的概念: 不能使用this来引用一个词法作用域内部的东西

  1. function foo() {
  2. var a = 2; //词法作用域
  3. this.bar(); //调用bar
  4. }
  5. function bar() {
  6. console.log( this.a ); //这样是不能获取到a的,根本都是未定义
  7. }
  8. foo(); // ReferenceError: a is not defined

3.1 this到底是什么?

之前我们说过 this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调 用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包 含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的 其中一个属性,会在函数执行的过程中用到。

3.2 找this调用位置

1) 确定this调用的位置(直接找对应的作用域属于那个调用栈)

  1. function baz() {
  2. // 当前调用栈是:baz
  3. // 因此,当前调用位置是全局作用域
  4. console.log( "baz" );
  5. bar(); // <-- bar 的调用位置
  6. }
  7. function bar() {
  8. // 当前调用栈是 baz -> bar
  9. // 因此,当前调用位置在 baz 中
  10. console.log( "bar" );
  11. foo(); // <-- foo 的调用位置
  12. }
  13. function foo() {
  14. // 当前调用栈是 baz -> bar -> foo
  15. // 因此,当前调用位置在 bar 中
  16. console.log( "foo" );
  17. }
  18. baz(); // <-- baz 的调用位置

3.2 绑定规则

1)默认绑定: 在哪里调用this,判断调用栈,执行this指向的对象; 全局作用域时,如果在非严格模式下,可以正常指定指定到window,如果严格模式下: 则指定的是undefined,不许允许只想window
2) 隐式绑定

  1. function foo() {
  2. console.log( this.a );
  3. }
  4. var obj = {
  5. a: 2,
  6. foo: foo
  7. };
  8. obj.foo(); // 2
  9. //对象属性引用链中只有最顶层或者说最后一层会影响调用位置。举例来说:
  10. function foo() {
  11. console.log( this.a );
  12. }
  13. var obj2 = {
  14. a: 42,
  15. foo: foo
  16. };
  17. var obj1 = {
  18. a: 2,
  19. obj2: obj2
  20. };
  21. //最后一层才会被受影响
  22. obj1.obj2.foo(); // 42

3) 隐式绑定存在this丢失的情况

  1. function foo() {
  2. console.log( this.a );
  3. }
  4. var obj = {
  5. a: 2,
  6. foo: foo
  7. };
  8. var bar = obj.foo; // 函数别名!
  9. var a = "oops, global"; // a 是全局对象的属性
  10. bar(); // "oops, global"

4) 显式绑定(call, apply):它们的第一个参数是一个对象,它们会把这个对象绑定到 this,接着在调用函数时指定这个 this。因为你可以直接指定 this 的绑定对象,因此我 们称之为显式绑定。(我去到这里的时候一不小ctrl+w了。没有保存,差点懵了,可以用ctrl+shift+T恢复哈)

  1. function foo() {
  2. console.log( this.a );
  3. }
  4. var obj = {
  5. a:2
  6. };
  7. foo.call( obj ); // 2 //这里就可以把foo对象的this指向obj
  8. 通过 foo.call(..),我们可以在调用 foo 时强制把它的 this 绑定到 obj 上。

其实也有可能把this丢失
装箱: 如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对 象,这个原始值会被转换成它的对象形式(也就是 new String(..)、new Boolean(..) 或者 new Number(..))。这通常被称为“装箱”。
5) 经典的硬绑定(是创建一个包裹函数,传入所有的参数并返回接收到的所有值)

  1. function foo(something) {
  2. console.log( this.a, something );
  3. return this.a + something;
  4. }
  5. // 简单的辅助绑定函数
  6. function bind(fn, obj) { //一个包裹函数
  7. return function() {
  8. return fn.apply( obj, arguments ); //传入所有的参数,病返回所有接收到的值
  9. };
  10. }
  11. var obj = {
  12. a:2
  13. };
  14. var bar = bind( foo, obj );
  15. var b = bar( 3 ); // 2 3
  16. console.log( b ); // 5
  17. //这个也就是Function.bind的用法
  18. Function.prototype.bind = (obj)=>{
  19. return function(){
  20. return this.apply(obj, arguments);
  21. }
  22. }

6) new绑定
使用new来调用对象,或者说发生构造函数调用时,实际上会自动执行下面的操作:

  1. 创建(或者说构造)一个全新的对象
  2. 这个新对象会执行原型[[prototype]]连接
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么new表达式中的函数会自动返回这个新对象

this的判定规则

现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。
可以按照下面的 顺序来进行判断:
1. 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。 var bar = new foo()
2. 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是 指定的对象。 var bar = foo.call(obj2)
3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。 var bar = obj1.foo()
4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到 全局对象。 var bar = foo()

被忽略的this

如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值 在调用时会被忽略,实际应用的是默认绑定规则:

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

4. 原型链[[prototype]]

4.1 [[Prototype]]

JavaScript 中的对象有一个特殊的 [[Prototype]] 内置属性,其实就是对于其他对象的引 用。几乎所有的对象在创建时 [[Prototype]] 属性都会被赋予一个非空的值。
注意:很快我们就可以看到,对象的 [[Prototype]] 链接可以为空,虽然很少见

4.1.1 属性设置和屏蔽

给一个对象设置属性并不仅仅是添加一个新属性或者修改已有的属性值。 现在我们完整地讲解一下这个过程:
myObject.foo = “bar”;
在于原型链上层时 myObject.foo = “bar” 会出现的三种情况。

  1. 如果在 [[Prototype]] 链上层存在名为 foo 的普通数据访问属性)并且没 有被标记为只读(writable:false),那就会直接在 myObject 中添加一个名为 foo 的新 属性,它是屏蔽属性。
  2. 如果在 [[Prototype]] 链上层存在 foo,但是它被标记为只读(writable:false),那么 无法修改已有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下,代码会 抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
  3. . 如果在 [[Prototype]] 链上层存在 foo 并且它是一个 setter,那就一定会 调用这个 setter。foo 不会被添加到(或者说屏蔽于)myObject,也不会重新定义 foo 这 个 setter。