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 就是全局的域
预编译四部曲
预编译发生在执行的前一刻。
- 创建AO对象(Activation Object:执行期上下文,全局对象会创建一个GO对象,GO === window)
- 将形参和实参作为AO对象的属性名,并赋值 undefined ,如果在全局中则忽略此步骤。
- 将实参和形参的值相统一。
- 将函数声明作为 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. 内部原理
- 在函数体最顶端隐式的创建一个
this={} - 执行
this.xxx = xxx - 隐式的
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
var num = 123; // 不可以拥有属性和方法var num = new Number(123); // 数字对象,可以拥有属性和方法
原始值 String 和 对象String
var str = 'abc'// 原始值字符串var str = new String('abc'); // 字符串对象
原值值 Boolean 和 对象Boolean
var bool = false; // 原始值布尔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();
原型链上属性的增删改查
- 增删改当前属性只能使用当前属性的原型来操作
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()方法