js作用域与执行环境(前端基础系列)
一、作用域(what?)
官方解释是:“一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。”
单从文字理解比较难懂,举个栗子:
function outer(){ // 声明变量 var name = "ukerxi"; // 定义内部函数 function inner() { console.log(name); // 可以访问到 name 变量 } } console.log(name); // 报错,undefined
其中变量name声明在 oute r函数中,当在 outer 中定义一个 inner 函数进行输出 name,可以得到正确的值,而在 outer 外进行输出 name 出现 undefined 错误;在此可以看出 outer 函数即为 name 变量的作用域(证明过程比较粗略,但结论还是正确的--_--);
二、作用域(why?)
作用域的使用,提高了程序的逻辑的局部性,增强程序的可靠性,以及避免命名冲突;为代码的模块化开发提供便利;根据上面提到的函数作用域,name 变量被局限在了 outer 函数中,在其他的函数中也可以定义相同名字的变量,两者之间不会互相影响;
三、js 中的作用域
先说ES5版本及更低版本的,因为在 ES6 上,重新定义了几个决定作用域的关键字;
- 没有块级作用域
在javaScript中,不像C、java等拥有块级作用域;常见块级作用域,例如:
// C语言实现 for(int i= 0; i<10; i++){ // 中括号里面就是块级作用域 } if(ture){ int i = 1; // 这里也是块级作用域 } printf("%d/n",i); // --“use an undefined variable:i” //这里是访问不到for语句中的i与if语句中的i变量的
当然这些在javaScript中是没有的,一般来说只有块级作用域;所以使用时须注意作用域的影响,例如:
for(var i= 0; i<10; i++){ // do something } console.log(i); // --10
在程序设计过程中可以使用函数作用域,进行模拟块级作用域;例如:
function loop(){ for(var i= 0; i<10; i++){ // do something } } loop(); console.log(i); // --undefined
javaScript是灵活可变的,同样上面这个例子,可以使用自执行函数重写实现,这样就减少了调用这一步;
(function (){ for(var i= 0; i<10; i++){ // do something } }()); console.log(i); // --undefined
- ES6中的作用域
在ES6中新增加了(let,const)关键字,进行定义变量,解决了没有块级作用域的限制;
let:let允许你声明一个作用域被限制在块级中的变量、语句或者表达式。与var关键字不同的是,它声明的变量只能是全局或者整个函数块的。let声明的变量只在其声明的块或子块中可用,这一点,与var相似。二者之间最主要的区别在于var声明的变量的作用域是整个封闭函数。
具体如下:
function range () { // let 和var 相同的地方,都有函数作用域 var name = 'ukerxi'; let nameOuter = 'outer'; for (var j = 0; j < 1; j++) { console.log(name) } console.log("输出j变量", j); // ==> 1 for (let i = 0; i < 1; i++) { console.log(nameOuter) } console.log("输出i变量", i); // 报错 undefined }
可以看出,使用let 定义的i变量,在for语句外进行输出时,会进行报错,说明i不在该作用域内,i的作用域在for包裹的作用内;
- 作用域链
每个函数都有自己的执行环境,包含当前环境的变量访问关系,与之相关联的就是“变量对象”,如果是当前函数的变量对象,也可称为“活动对象”;此对象中包含了,当前函数可访问的变量及函数;变量对象,最开始包含的对象是参数的arguments对象,然后是在函数中定义的其他变量及方法;例如:
function fn(name){ var text = "test"; } // 变量对象中包含:命名函数fn变量、参数name、内部变量test
当然这个变量对象是不可访问的,只提供后台引擎编译执行使用;当定义有多个变量对象嵌套,这些变量对象就组成了作用域链;例如:var name = "global"; function super() { var name = "super"; function sub(){ var name = "sub"; } }
作用域链:
在作用域最前端的是活动对象,而最后端是全局执行环境window(浏览器宿主中);变量访问原则是,根据作用域前端往上进行搜索,如果提前搜索到变量,则停止搜索,例如上面这个例子中,name变量的值是"sub"因为其在最前端的变量对象中已经定义了,就不会往上继续检索;
- 延长作用域
有两种方法可以将作用域进行延长:
①、try-catch 语句的catch块
②、with 语句
两个语句都是在原本的作用域最前端进行添加一个变量对象;例如:
var name = "global"; function test(){ var name = "sub"; with(window){ console.log(name); } } test(); // -- "global"
作用域链:
所以检索变量时,会先在最前端的window变量对象中检索;当然,在严格模式下已经禁用了with语句,编程时,最好向后兼容,废弃使用with语句; - 执行环境只与函数的声明及定义位置有关
当一个函数定义后其执行环境与作用域链就已经确定了,不会因为执行位置改变而改变,具体例子:
var name = "global"; function getName(){ console.log(name); } function test (){ var name = "inner"; getName(); } // 执行test test(); // -- global
运行test 函数,其中test 函数执行的是 getName 进行输出 name 变量,输出的是全局变量的信息;即当 getName 定义时就已经确定了自己的作用域及执行环境,因而不会因为执行位置的不同而输出不同的信息;当然有一种情况不一样,那就是灵活的 this
- this的动态绑定
与作用域链及执行环境不同,this是根据执行时的接受者进行绑定的,改变this的几种方法:
①、new 关键字
②、call / apply 方法
③、直接调用构造函数
具体例子如下:
// 声明一个类 function Person (){ this.name = "ukerxi"; } // 使用new关键字,使this执行新建对象 // 其实是构造函数默认返回this var men1 = new Person(); // this绑定到men1上 // 声明一个空对象,使用call/apply 进行绑定 var men2 = {}; Person.call(men2); // this绑定到men2上 // 直接执行构造函数 Person(); // this绑定到window上(使用严格模式则会报错,this指向undefined)
【结束语】
系列文章,包括了原创,翻译,转载等各类型的文章;一方面是为了自己总结,另一方面页希望可以共享知识;在技术方面有输入,也要有所输出,才能更进一步!文章基于自己的实践、阅读及理解,如有不合理及错误的地方,烦请各大佬评论指出,以便改正,感谢!