再学JavaScript(二)

151 阅读13分钟

11. Javascript 执行三部曲

1. 语法分析

通篇扫描是否存在语法错误


2. 预编译

预编译发生在函数执行的前一刻,有句话描述说:“函数声明整体提升,变量声明提升”,会将声明的函数和变量的声明,提升到当前逻辑的最顶端

console.log(a); // undefined,

var a = 1;

//因为变量声明已经提升到了逻辑的最前面,所以访问时输出 undefined 未赋值,而并不是 not defined

但是上面那句话的使用场景有限,不够严谨,在相对复杂的情况下不够用。

预编译前奏

imply global 暗示全局变量: 即任何变量在未经声明就赋值,此变量就为全局对象window所有。严格情况下这种不能称为变量,因为真正的变量是不可以通过delete关键字删除的,而这种可以,它是作为 window 的属性存在的。

a =  123;   // a 为全局变量
function ao() {
    var a = b = 123;   // b 为暗示全局变量 
}
console.log(window.a, window.b); 

一切声明的全局变量,全是window的属性。

var a  = 123;   // === window.a = 123;
b = 234;    		// === window.b = 234;
								// window 就是全局的域
预编译四部曲

预编译发生在执行的前一刻。

  1. 创建AO对象(Activation Object:执行期上下文,全局对象会创建一个GO对象,GO === window)
  2. 将形参和实参作为AO对象的属性名,并赋值 undefined ,如果在全局中则忽略此步骤。
  3. 将实参和形参的值相统一。
  4. 将函数声明作为 AO 对象的属性名,并将函数体作为该属性名的属性值。
function fn(a) {
      console.log(a);    // fn() {}
      var a = 123;
      console.log(a);    // 123
      function a() {};
      console.log(a);    // 123
      var b = function() {};
      console.log(b);    // fn() {}
      function d() {};
  }
  fn(1);

  /*
    1.创建AO对象
      AO{}

    2.将形参和变量声明作为AO对象的属性,并赋值 undefined
    AO{
    a : undefined   形参和变量相同时后者覆盖
    b : undefined
    }
    
    3.将实参和形参相统一
    AO{
    a : 1,
    b : undefined
    }
    
    4.将函数声明作为AO对象的属性,并赋值函数体
    AO{
    a : fn(){}
    b : undefined
    d : fn(){}
    }
  */

  function test() {
    console.log(b); // undefined
    if(a) {   // 预编译时会直接提取 if 中的变量声明
      var b = 100;
    }
    console.log(b; // undefined
    c = 234;
    console.log(c); // 234
  }
  var a;
  test();
  a = 10;
  console.log(c);

3. 解释执行


12 作用域

1. 作用域初探

作用域的定义:变量(变量作用域又称上下文)和函数生效(能被访问)的区域

1. 全局变量
var a = 10; // 全局变量
2. 局部变量
function test() {
  var b = 20;   // 局部变量
}    
3. 访问顺序

内部可以访问外部变量,外部不可以访问内部变量,自内向外


2. 作用域精解

1. 执行期上下文

当函数执行时,会创建一个称为 执行期上下文 的内部对象(AO)。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行期上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行期上下文,当函数执行完毕,它所产生的执行期上下文被销毁

2. 查找变量

从作用域的顶端依次向下查找

3. [[scope]]

每个javascript函数都是一个对象,对象中有些属性是我们可以访问(例如: functionName.name),但有些不可以,这些属性仅供javascript引擎存取,[[scope ]] 就是其中一个。

[[ scope ]] 指的就是我们所说的作用域,其中存储了执行期上下文的集合

4. 作用域链

[[ scope ]] 中所存储的执行期上下文对象的集合,这个集合呈链式链接,我们把这种链式链接叫做作用域链

代码示例1

// 每个函数被创建时,会根据当前的执行环境生成作用域链,保存在当前函数的 [[ scope ]] 属性作用域链中
// 当函数被执行时,会生成自己的作用域放在[[ scope ]]属性作用域链的最顶端
  
// 第一步. a函数被定义  生成:[[ scope ]]  存储内容: 0 : GO(Global Object)
/*
    1.
    事件:a 函数被定义
  生成当前执行期上下文[[scope]]
    存储的执行期上下文内容
      0 : Global Object

  */
function a() {       
/*
  3.
  事件:b 函数的定义
  生成自己的执行期上下文 [[scope]]
  存储的执行期上下文内容
  0 : AO(a 函数)
  1 : GO
*/
  function b() {	      
    var b = 234;
  }
  var a = 123;
  /*
    4.
    事件:b 函数被执行
    生成自己的执行期上下文存放进 [[scope]]
    存储的执行期上下文的内容
    0 : AO(b 函数)
    1 : AO(a 函数)
    2 : GO
  */
  b();
}
var glob = 100;
/*
  2.
  事件:a函数被执行
  将自己的执行期上下文添加到 [[scope]]
  存储的执行器上下文内容
    0 : AO(a函数)
    1 : GO
*/
a();     

代码实例2

function a() {
      function b() {
          function c() {
              
          }
          c();
      }
        b();
  }
  a();

  /*
    a defined a.[[scope]] --> 0:GO

    a doing   a.[[scope]] --> 0:AOa
                              1:GO

    b defined b.[[scope]] --> 0:AOa
                              1:GO

  b doing   b.[[scope]] --> 0:AOb
                              1:AOa
                              2:GO

  c defined c.[[scope]] --> 0:AOb
                              1:AOa
                              2:GO

    c doing   c.[[scope]] --> 0:AOc
                              1:AOb
                              2:AOa
                              3:GO
                              
所有的 GO、AOa、AOb、AOc 都是同一个执行上下文
当一个函数执行完毕时,相对应的执行上下文也会跟着销毁
  */

13. 闭包

当内部函数被保存到外部时,将生成闭包。闭包会导致原有作用域链不释放,造成内存泄漏。

作用

1. 实现公有变量
function add() {
    var count = 0;
    function demo() {
        count ++;
        console.log(count);
    }
    return demo;
}
var counter = add();
counter();
counter();
counter();
2. 做缓存
function test() {
      var num = 100;
      function a() {
          num ++;
          console.log(num);
      }
      function b() {
          num --;
          console.log(num);
      }
      return [a, b];
  }
  var myArr = test();
  myArr[0]();
  myArr[1]();

// -----------------------------------------------------------------------------

  function eater() {
      var food = "";
      var obj = {
          eat : function(){
              console.log("i am eating" + food);
          },
          push : function(myFood) {
              food = myFood;
          }
      }
      return obj;
  }
  obj.eat();
3. 实现封装,私有化属性。
4. 模块化开发,防止污染全局变量。

14. 立即执行函数

此类函数没有声明,在一次执行过后释放。适合初始化工作

// 立即执行函数执执行完之后立即销毁,除此之外和普通函数没有任何不同
// 针对初始化功能的函数
(function (a, b, c) {

    return a + b + c;

}(1, 2, 3));

// 可以使用变量接收返回值
var num = (function (a, b, c) {

    return a + b + c;

}(1, 2, 3));
执行符()

只有表达式才可以被执行符号执行。

立即执行函数扩展
// 立即函数不同写法
(function () {}());  // W3C推荐
(function () {})(); 

// 只有表达式才可以被执行符号执行
function test() {
  console.log('a');
}();  // 语法错误,此种方式不可以被执行,这种叫做函数声明

// 能被执行符号执行的函数,这个函数的名字就会被自动忽略
var a = function() {  // a 不再代表函数了
  console.log('a');
}();  // 可以,函数表达式

// ()、+(正)、-(负)、!、&&、|| 都可以将一个函数转换为表达式
- function() {
  console.log('a');
}(); // 可以正确执行
// ...

// !!! 注意
function test(a, b, c) {  // 在这中情况下,系统不会报错,但也不会执行
  console.log(a, b, c)
}(1, 2, 3);
// 系统会自动识别成这样
function test(a, b, c) {
  console.log(a, b, c);
}
(1, 2, 3); // 不识别成执行符号,看做一个独立的**逗号表达式(知识点再后续文档中呈现)**

15. 闭包续

1. 闭包的防范

闭包会导致多个执行函数共用一个公有变量,如果不是特殊需求,尽量防止这种情况发生。

2. 经典案例

// 给数组的每一位绑定一个函数,并打印出当前下标
function test() {
    var arr = [];
    for(var i = 0; i < 10; i ++) {
        arr[i] = function () {      // 赋值函数
            document.write(i + " ");
        }
    }
    return arr;  // 返回到外部,形成了闭包
}
var myArr = test();
for(var j = 0; j < 10; j ++) {
    myArr[j]();
}
// 执行结果 10 10 10 10 10 10 10 10 10 10 	
// 形成了闭包,由于在执行数组中的函数时,test 已经执行完毕。 i = 10并不再循环,这是for的判断条件
// 形成闭包执行,这10个函数访问的 i 在 test的AO里都已经变成了10

// 解决方法
 function test() {
   var arr = [];
   for(var i = 0; i < 10; i ++) {
     (function(n) {    // 使用立即执行函数,将每次的i用参数的进行进行套现,利用闭包解决闭包
       arr[n] = function() {
         document.write(n + " ")
       }
     }(i))
   }
   return arr;
 }
 var myArr = test();
 for(var j = 0; j < 10; j ++) {
     myArr[j](); // 执行每个方法
 }

16. 逗号操作符

逐个审查每一位元素。如果某位元素需要计算,则计算该元素。最后,返回最后一个元素的计算结果。

// 逐个审查每一位元素。如果某位元素需要计算,则计算该元素。最后,返回最后一个元素的计算结果
var a = (2, 3);
var f = (
    function f() {
    	return '1';
	},
    function g() {
        return 2;
    }
)();
typeof(f);

17. 对象

Object是一种基础的变量类型,属于引用值

var mrZhang = {
 name : 'zs',
 age : 22,
 sex : 'male',
 health : 100,
 smoke : function() {
   console.log('I am somkeing');
   mrZhang.health --; // this.health  this表示当前,第一人称
 }
}

1. 属性的增删改查

mrZhang.wife = 'xiaoliu'

delete mrZhang.wife

mrZhang.sex = 'female'

mrZhang.name  // 如果对象没有name属性的话会返回undefined(变量没有声明的话会报错)

2. 对象的创建方法

字面量

var obj = {};    // plainObject 对象字面量/对象直接量

构造函数

1. 系统自带
new Object();  // 得出相同,且相互独立的对象
/*
  Array();
  Number();
  Boolean();
  String();
  Date();
*/
2. 自定义案例
function Car(color) { // 为区分自定义函数和普通函数,使用大驼峰命名法
  this.color = color; // 使用参数实现自定义
  this.name = 'BMW';
  this.height = '1400';
  this.lang = '4900';
  this.weight = '1000';
  this.health = 100;
  this.run = function() {
    this.health --;
  }
}
var car1 = new Car('red');
var car2 = new Car('green');

3. 构造函数解析

1. 内部原理

  1. 在函数体最顶端隐式的创建一个 this={}
  2. 执行 this.xxx = xxx
  3. 隐式的 return this
    • 可以手动显式的返回,return({}, [])只可以返回类型为object的值,原始值无效,自动忽略,有 new不可能返回原始值。
// new 之后函数的变化
function Student(name, age, sex) {
  /*
    1. var this = {}, AO{ this:{name:'zhagnsan'} }  // 隐式创建一个this对象
  */

  /* 2. */
  this.name = name;
  this.age = age;
  this.sex = sex;
  this.grade = 2017;

  /*
    3. return this;  隐式 return this;
  */
}
var student = new Student('zhangsan', 22, 'male');

/*
  1. 在函数体最前面隐式的创建 this = {};
  2. 执行 this = xxx;
  3. 隐式的 return this;
    3.1. 可以手动显式 return ({}, [], /..)类型为obj的值,原始值是无效,自动忽略的
*/

2. 模拟构造函数

// 模拟构造函数(只是简单模拟,并不推荐使用。因为还有更深层次的东西模拟不了)

  function Person(name, height) {
      var that = {}; // 模拟构造函数中的 this
      that.name = name;
      this.height = height;
      return that; 
  }
  var person1 = Person('xiaowang', 180);

18. 包装类

原始值是坚决不能用属性和方法的,但是通过包装类可以给原始值设置属性即方法

基本原理

1. 理解原始值和对象

原始值Nubmer和对象Number

  1. var num = 123; // 不可以拥有属性和方法
  2. var num = new Number(123); // 数字对象,可以拥有属性和方法

原始值 String 和 对象String

  1. var str = 'abc' // 原始值字符串
  2. var str = new String('abc'); // 字符串对象

原值值 Boolean 和 对象Boolean

  1. var bool = false; // 原始值布尔
  2. var bool = true; // 布尔对象

2. 原始值可以设置属性和方法的原理

// 包装类的执行过程

var num = 4;   
// 原始值是坚决不能用属性的

// new Number(4).len = 3;
num.len = 3;
// 如果系统识别你要给原始值添加属性是不会报错的,则会调用 new Number(4).len = 3; 进行设置
// 设置完成后会立即 delete 删除此属性	

// new Number(4).len   undefined // 删除后则访问不到了
console.log(num.len);

// 在我们访问字符串的length 属性时 new String('abc').length 将其长度进行返回。但这个长度不可以设置,如果进行手动设置则同样会走包装类的流程

var str = 'abc';
var str.length = 2;

// console.log(str.length);
console.log(new String(str).length);

// 题目练习
var str = 'abc';
str += 1;
var test = typeof(str);
if(test.length == 6) {
  test.sign = 'typeof 的返回结果可能是String';
}
console.log(test.sign);

19. 原型

原型是 function 对象的一个属性,它定义了构造函数制造出的对象的公共祖先。通过该构造函数产生的对象,可以继承原型的属性和方法,原型也是对象。原型只执行一遍。

示例

// Person.prototype  -- 原型
// Person.prototype = {} 就是Person构造函数构造出对象的公共祖先
Person.prototype.name = 'hehe';
function Person() {
     this.name = 'zs';  // 如果构造函数中存在和原型中相同的属性,会优先使用调用构造中的属性
}
var person1 = new Person(); // 都继承了原型的 name 属性
var person2 = new Person();


 
Person.prototype = {   // 也可以这样定义原型
    height : 180,
    //...
}

1. 公用属性

利用原型的特点和概念,可以提取共有属性

示例
Car.prototype = {   // 将公有部分提取到原型中,可以提升性能,如果写在构造函数中每一次new都得执行一遍
    naem : 'BMW',
    height : 1400
    //...
}
function Car(color) {
    this.color = color;
}
var car1 = new Car('red');

2. 原型的增删改查

Person.prototype.age = 18

delete Person.prototype.name

通过delete parson1.name删除的是自身对象中的属性,如果没有也不会报错。

Person.prototype.name = "changePrototypeName"

需要通过原型来修改,如果person1.name会在自身对象中添加name 属性 并赋值 changePrototyeName。

person1.name

示例

Person.prototype.lastName = "Zhang";
Person.prototype.constructor = Student;
function Person(name) {
  this.name = name;
}
var person1 = new Person("Howie");
// 原型的 增、删、改、查
// Person.prototype.sex = 'male'
// delete Person.prototype.sex
// Person.prototype.sex = 'female';  
// Person.prototype.sex = 'male'

3. constructor

查看对象的构造函数

示例
function Car() {
     
}
var car1 = new Car();
console.log(car1.constructor); // 这个方法也是在原型中继承过来的,可以手动更改
/*
 Car.prototype
   {constructor: ƒ}
     constructor: ƒ Car() 
     __proto__: Object 
 */

// 手动修改 constructor 属性的指向

function Student() {

}
Person.prototype.lastName = "Zhang";
Person.prototype.constructor = Student;
function Person(name) {
  this.name = name;
}
var person1 = new Person("Howie");

4. __proto__

如何查看原型 -> 隐式属性

示例
//  查看 person1.__proto__ 
Person.prototype.name = 'zs';
function Person() {
/*
  var this = {
  	__proto__ : Person.prototype, 
  	当发生 new 的时候,this对象中就会存在__proto__属性
    也就是说,如果 person1 查找属性,在Person构造中不存在的话,就会通过__proto__属性所绑定的原型来查找,这是系统提供的属性
    __proto__ 就是指向原型的指针
   }
*/
}
var person1 = new Person(); 
console.log(person1.name);

5. 本章总结

我们需要牢记两点:

__proto__constructor属性是对象所独有的;

prototype属性是函数所独有的,因为函数也是一种对象,所以函数也拥有__proto__constructor属性。

__proto__属性的作用就是当访问一个实例对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__指针所指向的那个对象(父对象)里找,一直找,直到__proto__属性的终点null,再往上找就相当于在null上取值,会报错。通过__proto__属性将对象连接起来的这条链路即我们所谓的原型链。


20. 原型链

如何构成原型链

// 手动的更改构造函数的原型,连成一个链,称为原型链

// Grand.prototype 上指向 Object.prototype, Object.prototype是所有原型的最终原型  Object.prototype.__proto__ : null
function Grand() {
    this.grandName = 'grand'
}
var grand = new Grand();

Father.prototype = grand;
function Father() {
    this.fatherName = 'father';
    this.fortune = {
        car1: 'visa'
    }
}
var father = new Father();

Son.prototype = father;
function Son() {
    this.sonName = 'son';
}
var son = new Son();

原型链上属性的增删改查

  1. 增删改当前属性只能使用当前属性的原型来操作
  2. son.fortune.car2 = "master" 利用原型中的引用类型可以修改

绝大多数对象最终都会继承自 Object.prototype 但是有个一除外 Object.create()

// 执行
Object.create()

/**
* 错误信息
* VM100:1 Uncaught TypeError: Object prototype may only be an Object or null: undefined
*    at Function.create (<anonymous>)
*    at <anonymous>:1:8
*/ 
                        
Object.create(null);  // 创建出的对象不会继承自 Object.prototype
Object.create(原型, 特性)
// 创建一个对象并指定原型
// var obj = object.create(原型)

var obj = {name : 'zs', age : 22, sex : 'male'};
var student = Object.create(obj);

Person.prototype.name = 'ls';
function Person() {
}
var obj1 = Object.create(Person.prototype);

21. 拓展 - toString()

toString()

/*
原型都是继承自最终原型 object.prototype 的,而原型中存在一个toString()方法

引用值可以调用toString(),部分原始值也可以调用toString()(Number、String、Boolean这些类型可以通过包装类来实现调用toString()方法),但是 undefined 和 null 是没有包装类的

document.write() 会隐式调用 toString()方法

一下方法都重写了toString方法
  Number.prototype.toString()
  String.prototype.toString()
  Boolean.prototype.toString()
*/

var obj = {};
document.write(obj); // 隐式的调用toString()方法,[Object object]

var obj1 = Object.create(null);
// 手动重写 toString() 
obj1.toString = function() {
    return 'hehe';
}
document.write(obj1);// 1. 报错,因为obj1没有原型、没有toString()方法