JS的执行机制
- 本人小盆友一枚,本系列为个人学习总结,主要为基础知识,主要目的在于督促自己建立完整的知识体系,查缺补漏,回归基础;
- 错误肯定有,需进一步完善,分享出来,希望对于自己和他人有用;
- 错误之处,请留言,我会及时修正;
- 此外,因此系列文章为基础知识,大佬可略过,希望不会浪费你的时间;
一、javascript的编译和执行
执行顺序是按照脚本标签
<script>
出现的顺序来确定的,代码是按从上到下的顺序执行的。变量在预编译时会被赋初始值undefined,如果一直没有被赋值,就会沿用这个值;
javascript并非是完全按照顺序执行的,而是在执行之前先进行一个预编译,预编译时声明式函数(语句定义法)被提取出来,优先执行,而且相同的函数会进行覆盖,再执行赋值式函数。
javascript中的给个代码块是相互独立的,当脚本遇到第一个
<script>
标签时,则javascript 解析器会等这个代码块加载完成后,先对它进行预编译,然后再执行它,然后javascript解析器准备解析下一个代码块,由于javascript是按块执行的,所以一个javascript调用下一个块的函数或者变量时,会出现错误。虽然javascript是按块执行的,但不同的块却属于相同的全局作用域,不同的块的变量和函数是可以相互使用的,也就是某个块可以使用前面块的变量和函数,却不可以使用它之后的块的变量和函数。
二、声明提升(Hoisting)
提升(Hoisting)是 JavaScript 默认将当前作用域提升到前面去的的行为。
提升的类型:
(1)变量声明 - 部分提升;
var b;
console.log(a); console.log(b); console.log(b);
var b = 2; b = 2;
// ReferenceError: a is not defined // undefined // undefine
(2)函数声明 - 完全提升。因此,函数可以在声明之前调用:
示例:
// 函数声明,提升后,代码正常运行
var result = add(5, 5);
console.log(result); // 10
function add(num1, num2) {
return num1 + num2;
}
// 函数表达式,没有提升,报错
var result = add(5, 5);
console.log(result); // TypeError: add is not a function
var add = function(num1, num2) {
return num1 + num2;
};
三、作用域
作用域:变量的可访问范围就是变量的作用域;
(一)作用域类型
(1)全局作用域
不在任何函数内部的作用域, 叫全局作用域,对应的是全局变量;(避免使用全局变量)
全局变量:可以整个脚本域中有效。
(2)函数作用域
函数自身的作用域叫局部作用域,也可以叫函数作用域,对应的是局部变量;
在函数中以var关键字定义的变量就叫做局部变量,局部变量只能在所在的函数内部有效;
局部变量有两种:1. 在函数中使用var定义的变量;2. 函数的参数也是该函数的局部变量
(3)块级作用域(ES6)
以后,再具体写ES6,自己也正在学习ing……
用let和const声明的绑定,所以如果你在一个循环中创建了一个绑定,循环之前和之后的代码不能“看到”它。在2015版JavaScript之前,只有函数创建一个新的范围,用var创建的旧式绑定通过整个绑定所在的函数是可见的,
let x = 10;
if (true) {
let y = 20;
var z = 30;
console.log(x + y + z); //60
}
// y 不可见
console.log(x + z); //40
补充:执行上下文context
可执行代码类型:全局代码、函数代码、eval代码;
执行上下文类型:全局上下文、函数上下文、eval上下文;
(二)变量的访问机制
在内部(局部)作用域中可以访问到外部(全局)作用域中的变量或函数;而在外部无法访问(使用)到内部作用域中的变量或函数;
如果变量是在某个函数中定义的,那么它在函数以外的地方是不可见的。而如果该变量是定义在if或者for这样的代码块中,它在代码块之外是可见的。
在js中,“全局变量”指的是声明在所有函数之外的变量;“局部变量”指的是在某个函数中定义的变量;函数内的代码可以可以访问全局变量。
回收机制:函数内部的变量,在函数执行完成之后时会销毁的;
注意:
- 在函数内部优先会使用本作用内的变量, 如果本作用域内没有,那么向外面去找;
- 函数传参本质是值的传递
var a = 123;
function foo(){
console.log(a); //undefined
var a = 1;
console.log(a); // 1
}
foo();
(三)嵌套作用域 - 闭包 Closure
函数之间是可以嵌套调用的;(函数的嵌套调用:在一个函数体的内部调用另外一个函数, 就成为函数嵌套调用;)
闭包:能够引用封闭函数中局部绑定的特定实例——称为闭包。
function wrapValue(n) {
var local = n;
return function () {
return local;
};
}
var wrap1 = wrapValue(1);
var wrap2 = wrapValue(2);
console.log(wrap1()); //1
console.log(wrap2()); //2
- 示例:
function multiplier(factor) {
return function (number) {
return number * factor;
};
}
var twice = multiplier(2);
console.log(twice(5)); //10
四、this 对象
(一)this
首先,写一个示例;
var person = {
name: "Nicholas",
sayName: function() {
console.log(person.name);
}
};
person.sayName(); // "Nicholas"
这种写法存在的问题:
首先,如果你改变了变量名,你还需要记得更改该变量名在方法中的引用。
第二,这种紧密耦合使得难以为不同的对象使用相同的函数。
在JavaScript中,每个作用域都有一个this对象,它表示这个函数的调用对象,可以理解为一个对象的指针;
在全局作用域里,this代表全局对象(在web浏览器里是window)。
当一个函数被调用时,同时函数会被绑定到一个对象上,此时,this的值默认等于那个对象。在一个方法里,可以通过引用this来替代直接引用该对象。
- 例如:使用this,重写上面的示例
var person = {
name: "Bumphy",
sayName: function() {
console.log(this.name);
}
};
person.sayName();// "Bumphy"
上面的例子中,sayName()引用this替代person。这意味着可以更容易改变变量名或者甚至复用这个函数在不同的对象上。
function sayNameForAll() {
console.log(this.name);
}
var person1 = {
name: "Bumphy",
sayName: sayNameForAll
}
var person2 = {
name: "Oska",
sayName: sayNameForAll
};
var name = "Michael";
person1.sayName();//"Bumphy"
person2.sayName();//"Oska"
sayNameForAll(); //"Michael"
在这个例子的最后部分定义了一个全局变量 name。当直接调用sayNameForAll()时,它会输出“Michael”,因为全局变量被认为是全局对象的一个属性。
(二)改变this
通常,对象的this是自动指派的,但是可以改变它的指向(值)来达到不同的目标。有三个方法可以改变this的值;
(1)call()方法
第一个操作this的函数方法是call(),它使用特定的值和特定的参数执行函数。
call()的第一个参数是执行该函数时应该相等的值。随后的参数是应该传递进函数的参数,需要单独命名每个参数
- 例如,假设你更新sayNameForAll()来获取一个参数:
function sayNameForAll(label) {
console.log(label + ":" + this.name);
}
var person1 = {
name: "Nicholas"
};
var person2 = {
name: "Greg"
};
var name = "Michael";
sayNameForAll.call(this, "global"); // "global:Michael"
sayNameForAll.call(person1, "person1"); // "person1:Nicholas"
sayNameForAll.call(person2, "person2"); // "person2:Greg"
(2)apply()方法
apply()方法和call()方法是一样的,
apply()方法只接受两个参数:this值和一个数组或类数组的对象,作为参数传递给函数(意味着你可以使用一个arguments对象作为第二个参数)。
- 示例,apply()方法:
function sayNameForAll(label) {
console.log(label + ":" + this.name);
}
var person1 = {
name: "Bumphy"
};
var person2 = {
name: "Oska"
};
var name = "Michael";
sayNameForAll.apply(this, ["global"]); // "global:Michael"
sayNameForAll.apply(person1, ["person1"]); // "person1:Bumphy"
sayNameForAll.apply(person2, ["person2"]); // "person2:Oska"
使用哪个方法通常取决于数据的类型。如果你已经有了一个data数组,则使用apply();如果你只有个别的变量,使用call()。
(3)bind()方法
这个方法是在ECMAScript5新增的,它和其他两种方法不同。
bind()的第一个参数是新函数的this值。所有其他参数代表应该在新函数中永久设置的命名参数。你仍然可以传入任何以后不会永久设置的参数。
- 使用bind()的两个示例。
通过将this值绑定到person1来创建sayNameForPerson1()函数,而sayNameForPerson2()将this 绑定到person2并将第一个参数绑定为“person2”
function sayNameForAll(label) {
console.log(label + ":" + this.name);
}
var person1 = {
name: "Bumphy"
};
var person2 = {
name: "Oska"
};
// 为person1创建一个函数
var sayNameForPerson1 = sayNameForAll.bind(person1);
sayNameForPerson1("person1"); // "person1:Bumphy"
// 为person2创建一个函数
var sayNameForPerson2 = sayNameForAll.bind(person2, "person2");
sayNameForPerson2(); // "person2:Oska"
// 将方法附加到对象不会改变“this”
person2.sayName = sayNameForPerson1;
person2.sayName("person2"); // "person2:Bumphy"
没有参数绑定为sayNameForPerson1(),所以你仍然需要 传递输出的标签。
函数sayNameForPerson2()不是只将此绑定到person2,但也将第一个参数绑定为“person2”。这意味着可以调用sayNameForPerson2()而不传入任何其他参数。
这个例子的最后一部分增加了sayNameForPerson1() 到名为sayName的person2上。该函数是绑定的,所以值 这不会改变,即使sayNameForPerson1现在是一个函数 PERSON2。该方法仍输出person1.name的值。
最后
- 参考书籍:《The Principles of object-oriented JavaScript》,读英文版;