1 JavaScript作用域与闭包
1.1 编程语言的作用域规则
1.1.1 词法作用域:
词法作用域:定义在词法阶段的作用域。函数和变量的作用域由书写代码时,函数和变量的声明位置决定。
可再细分为函数作用域,块作用域。
与调用位置无关,与定义的位置有关
示例1:
var a=0;
function f1(){
var a=1;
f2();
}
function f2(){
console.log(a);
}
f1();
实例中的f2函数中的a变量的值,会在定义f2()函数的上层作用域查找a。
即,找到全局作用域中的 a=0,输出结果0
1.1.2 动态作用域:
动态作用域:作用域是基于调用栈的,在代码runtime的时候才决定,灵活多变,但是无法提前准确预知。例如shell语言
故和示例1相同情况的声明和调用,输出结果为1。
因为会进入函数的调用栈,当打印的函数找不到变量值,查找上层f1的调用栈,并在上层调用栈中,查找到对a的赋值。
因此,这种情况下,输出值与调用函数直接相关
1.2 函数作用域与块作用域
都属于语法作用域
①函数作用域(var声明)
函数内定义的变量,在整个函数内有效,附加效果:变量提升,浏览器中全局声明会添加到windows对象。
②块作用域(let const声明)
定义的变量只在变量所在的代码块有效,附加效果:无变量提升+暂时性死区、在for循环中绑定每一个迭代。
let声明的变量可以重新赋值,const不可以。
示例2:
function showVar(){
if(true){
var a=1;
}
typeof a != 'undefined'?console.log(a):console.log('a is undefined')
}
showVar();
function showLet(){
if(true){
let a=1;
}
typeof a != 'undefined'?console.log(a):console.log('a is undefined')
}
showLet();
var声明的变量 作用域为函数,故输出1
let只作用于if控制语句的代码块。输出a is undefined
1.3 附加效果
①变量提升(var)
示例3:
function showVar(){
console.log(a)
if(true){
var a=1;
}
}
showVar();
function showLet(){
console.log(a)
if(true){
let a=1;
}
}
showLet();
第一个函数输出undefined 变量a已定义,但是未赋值。
等价于var a=1;提升到作用域顶部,
第二个函数输出ReferenceError: a is not defined 报错
typeof 的安全防范机制:使用typeof 访问未定义的变量,可以防止抛出ReferenceError。
②暂时性死区(let const)
let const声明变量时,会在当前的块作用域开始处直到变量声明之前形成暂时性死区,在该区域内,不可引用未定义的变量,即使是typeof安全机制也会失效。
示例4:
function showLet(){
if(true){
typeof a != 'undefined'?console.log(a):console.log('a is undefined')
let a=1;
}
}
showLet();
输出:ReferenceError: Cannot access ‘a’ before initialization
③在for循环中绑定每一次迭代
示例5:
function showVarFor(){
for(var i=0;i<5;i++){
setTimeout(()=>{
console.log(i);
},i*100);
}
}
showVarFor();
function showLetFor(){
for(let i=0;i<5;i++){
setTimeout(()=>{
console.log(i);
},i*100);
}
}
showLetFor();
var输出:5 5 5 5 5
let输出:0 1 2 3 4
这里设置了五个延时函数,再使用var声明时,设置好五次延时函数,i已经自增到5。再次访问i,输出全是5。let声明会绑定循环的每一次迭代。
可以通过函数传参的形式,将参数传入函数作用域,以复制的形式保存下每次循环的值,故而使用var也可以进行正常使用。
function showVarFor(){
for(var i=0;i<5;i++){
function test(j){
setTimeout(()=>{
console.log(j);
},j*100);
}
test(i);
}
}
showVarFor();
此时输出:0 1 2 3 4
1.4 闭包
当一个变量“脱离”了声明时所处的作用域而存在时,这个变量就成了闭包变量
闭包的好处:延长变量的生命周期,让函数可以保存状态,并且对外隐藏状态
闭包的应用:定义模块、函数柯里化…
js中闭包无处不在
**闭包的坏处:**变量无法释放,引发内存泄露
**解决:**及时手动释放变量
1.4.1保持内部状态
闭包让函数保持状态,传统面向对象的思路,使用类和实例来保存内部状态。这一点可以利用闭包实现。
function CreatePerson(){
var myName;
return{
setName(name){
myName=name;
},
getName(){
return myName;
}
};
}
person=CreatePerson();
person.setName('james');
console.log(person.getName());
console.log(person.myName);
james
undefined
myName可以看做内部状态
1.4.2 函数柯里化
//计算立方体体积
function calculateVolumeFactory(x,y){
const area=x*y;
return function(z){
return area*z;
}
}
const calculateVolumeWithArea=calculateVolumeFactory(10,20);
console.log(calculateVolumeWithArea(30));
console.log(calculateVolumeWithArea(40));
6000
8000
1.4.3 利用闭包模拟模块定义
用模块容器存储模块变量,存储每一个模块,通过get得到,进行调用。
//例子三、利用闭包模拟模块定义
const Modules = ( function Manager(){
//闭包变量模块对象容器
var modules = {};
/***
*@description :定义模块
*@aparam {string} name模块名
*@param {string[]} deps模块依赖数组
*@param : {function} impl 模块实现函数,用来定义模块、创建返回模块对象
*/
function define(name, deps, impl) ,{
for (var i=0;i< deps.length; i++){
if(!modules[deps[i]]){
throw new Error( `${modules[deps[i]]} is not defined, canot init ${ name}` );
}
deps[i]. = modules [ deps[i]];
modules[name ],= impl.apply(impl, deps);
}
/**
*@description获取指定模块对象
*@param {string} name模块名
*@returns {object} 模块对象
*/
function get(name){
}
return {
define,
get,
}
})();
//定义bar模块
Modules.define( 'bar', [],function() {
function hello() {
console.log( 'hello' );
}
//返回bar模块对象
return {
hello,//对外暴露一个hello方法
});
//定义foo模块
Modules .define( 'foo', ['bar'], function(bar) {
function world(){
bar.hello();
console.log('world');
}
//返回foo模块对象
return {
world,//对外暴露一个world方法
};
});
foo.Modules.get('foo');
foo.world();
//手动释放
foo=null;
bar=null;
输出 hello world
2 this指针
并不遵守词法作用域
影响因素:
①方法是否由某个对象调用(method) : obj.test()
②是否是独立函数调用(function) : test()
③是否使用函数的apply、call. bind、 等方法进行硬绑定test.apply(obj);
④是否使用了ES6的箭头函数: ()=>{…}
⑤是否使用了new关键字调用函数: new test()
⑥浏览器中的全局this指针
//放在浏览器中执行
//1全局this指针
console.log(this); //输出window对象
//2作为method调用
var a = {
logThis(){
console.log(this);
}
}
a.logThis(); //输出a
///a的附属方法的调用
//3
var a={
logThis() {
console.log(this);
}
}
var logThis4 = a.logThis;
logThis4();//输出window对象
///相当于一个全局对象的一个调用,不再是a的附属方法的调用,this不再指a
//4作为function调用 不属于严格模式
function logThis() {
console.log(this);
}
logThis(); //输出window对象
//5严格模式
'use strict';
function logThis2() {
console.log(this);
}
logThis2(); //输出undefined
//使用apply、 call、bind,
function logThis2(x, y, z) {
console.log(this, x, y, z);
}
var obj={msg: 'this is obj'};
logThis2.ply(obj,[1, 2, 3]); //输出obj
logThis2.call(obj, 1, 2, 3); //输出obj
var logThis3 = logThis2.bind(obj, 1, 2);
LogThis3(3); //输出obj
//,apply、call、bind函数都可以用来显式指定函数的this指针。其中apply、 call函数会在绑定this指针后,立即调用函数
// apply、 call之间的差别主要在于绑定this之后, 传入参数的方式不同,apply需要使用数组的方式传入参数,call不需要。
// bind除 了绑定this指针, 还可以绑定参数
//apply的这种特性常常被用来做数组解耦,bind的特性则常用来做预置参数 (与函数柯里化有点类似)
//使用箭头函数
var obj2={
test() {
console.log(this);//输出obj2 因为test被obj2调用
var obj={ !
test2:()=>{
console.log(this);
},
test3(){
console.log(this);
}
}
obj.test2();//输出obj2 箭头函数没有自己的this,只能继承外层的this指针,称为this穿透。
obj.test3();//输出obj 普通函数 作为obj的方法,被调用所以指向obj
}
}
obj2.test(); //输出
箭头函数没有自己的this,只能继承外层的this指针,称为this穿透。
//new
function logThisk) {
console.log(this);
}
new logThis(); // 输出logThis对象
LogThis(); // 输出window对象
new关键字会在函数内部创建一个新对象,然后将this指针指向这个新对象。
补充
浏览器中,全局中的this指向全局对象 ;node环境下,全局this指向空
作为独立函数调用时,严格模式下绑定到undefined,否则绑定到window
作为某个对象的方法调用时,指向该对象
New VS call、apply、bind VS箭头函数
new、箭头函数 > bind >apply、call
①箭头函数的优先级大于call、apply、 bind, 无法使用call、apply、bind来改变箭头函数的this,
箭头函数不可以与new关键字起使用。 箭头函数没有自己的this指针, 无法作为构造函数。
②new的优先级比bind高,井且new”无法与apply和call一起使用。
③bind的优先级比call和apply都要高。