作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。
function foo() {
var a = 1
}
foo()
console.log(a) // Uncaught ReferenceError: inVariable is not defined
上面例子可以理解为:作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
在JavaScript中,作用域可以分为
全局作用域
函数作用域
ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6的到来,为我们提供了‘块级作用域’,可通过新增命令let和const来体现。
块级作用域可通过新增命令let和const声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:
在一个函数内部
在一个代码块(由一对花括号包裹)内部
首先认识一下什么叫做 自由变量 。如下代码中,console.log(a)
要得到a变量,但是在当前的作用域中没有定义a(可对比一下b)。当前作用域没有定义的变量,这成为 自由变量 。自由变量的值如何得到 —— 向父级作用域寻找(注意:这种说法并不严谨,下文会重点解释)。
如果父级也没呢?再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是 作用域链 。
let a = 'global';
console.log(a);
function course() {
let b = 'zhaowa';
console.log(b);
session();
function session() {
let c = 'this';
console.log(c);
teacher();
function teacher() {
let d = 'yy';
console.log(d);
console.log('test1', b);
}
}
}
console.log('test2', b);
course();
if(true) {
let e = 111;
console.log(e);
}
console.log('test3', e)
许多开发人员经常混淆作用域和执行上下文的概念,误认为它们是相同的概念,但事实并非如此。
JavaScript属于解释型语言,JavaScript的执行分为:解释和执行两个阶段,这两个阶段所做的事并不一样:
解释阶段:
词法分析
语法分析
作用域规则确定
执行阶段:
创建执行上下文
执行函数代码
垃圾回收
JavaScript解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的。执行上下文最明显的就是this的指向是执行时确定的。而作用域访问的变量是编写代码的结构确定的。
作用域和执行上下文之间最大的区别是:
执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变。
this的绑定实际是函数被调用时才发生的绑定,也就是this指向什么,取决于你如何调用函数.
this是在执行时动态读取上下文决定的,而不是创建时
默认绑定
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
默认绑定:将全局对象绑定this
注意:在严格模式下(strict mode),全局对象将无法使用默认绑定,即执行会报undefined的错误
隐式绑定
除了直接对函数进行调用外,有些情况是,函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。
function foo() {
console.log( this.a );
}
var a = 2;
var obj = {
a: 3,
foo: foo
};
obj.foo(); // 3
隐式丢失(回调函数)
function foo() {
console.log( this.a );
}
var a = 2;
var obj = {
a: 3,
foo: foo
};
setTimeout( obj.foo, 100 ); // 2
同样的道理,虽然参传是obj.foo,因为是引用关系,所以传参实际上传的就是foo对象本身的引用。对于setTimeout的调用,还是 setTimeout -> 获取参数中foo的引用参数 -> 执行 foo 函数,中间没有obj的参与。这里依旧进行的是默认绑定。
显示绑定
call
或apply
或bind
function foo() {
console.log( this.a );
}
var a = 2;
var obj1 = {
a: 3,
};
var obj2 = {
a: 4,
};
foo.call( obj1 ); // 3
foo.call( obj2 ); // 4
call
跟apply
的用法几乎一样,唯一的不同就是传递的参数不同,call
只能一个参数一个参数的传入。apply
则只支持传入一个数组,哪怕是一个参数也要是数组形式。最终调用函数时候这个数组会拆成一个个参数分别传入。
至于bind
方法,他是直接改变这个函数的this
指向并且返回一个新的函数,之后再次调用这个函数的时候this
都是指向bind
绑定的第一个参数。bind
传餐方式跟call
方法一致。
Function.prototype.myBind = function() {
const _this = this
// 复制参数
const args = Array.prototype.slice.call(arguments);
const newThis = args.shift();
return function() {
return _this.apply(newThis, args)
}
}
Function.prototype.myApply = function(context) {
// 参数检测
context = context || window;
// 指向挂载函数
context.fn = this;
//
}
是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象
是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是 指定的对象。
是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。
如 果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到 全局对象。
function foo() { console.log( this.a );}foo.call( null ); // 2foo.call( undefined ); // 2
var foo = () => {
console.log( this.a );
}
var a = 2;
var obj = {
a: 3,
foo: foo
};
obj.foo(); //2
foo.call(obj); //2 ,箭头函数中显示绑定不会生效
function foo(){
return () => {
console.log( this.a );
}
}
var a = 2;
var obj = {
a: 3,
foo: foo
};
var bar = obj.foo();
bar(); //3
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
函数作为返回值的场景
function mail() {
let content = '信';
return function() {
console.log(content);
}
}
const envelop = mail();
envelop();
函数作为参数
// 单一职责
let content;
// 通用存储
function envelop(fn) {
content = 1;
fn();
}
// 业务逻辑
function mail() {
console.log(content);
}
envelop(mail);
函数嵌套
let counter = 0;
function outerFn() {
function innerFn() {
counter++;
console.log(counter);
// ...
}
return innerFn;
}
outerFn()();
事件执行
let lis = document.getElementsByTagName('li');
for(var i = 0; i < lis.length; i++) {
(function(i) {
lis[i].onclick = function() {
console.log(i);
}
})(i);
}
56 天前