JS三部曲,变量提升,this与作用域,闭包

1,175 阅读8分钟

这篇文章总结下之前看的文章和自己在工作中遇到的坑,纯手写的,有什么写的不对的请多多提出修正哈

变量提升

何为变量提升?js里面的变量在声明之前就可以使用,因为该声明已经被提升至该作用域(函数或全局)的顶部 直接上代码

function fn1(){
    console.log(a)
    var a=10;
    function a(){};
    console.log(a)
}
fn1();
//var和function都会变量提升,优先级function>var,上面可理解为
function fn1(){
    var a;
    function a(){};
    console.log(a);//a函数
    a=10;
    console.log(a);//10
}

这时我们加上一个参数来比较

function fn1(a){
    console.log(a);
    var a=20;
    function a(){};
    console.log(a);
}
fn2(10);//打印出函数a
//变量提升有个优先级:函数a>参数>变量,上面可理解为
function fn2(a){//a=10
    var a;
    //a被参数的a赋值为10或者理解为后声明的覆盖不了前面已赋值的
    function a(){};//变量a变为函数a
    console.log(a);//函数a
    a=20;//a重新赋值为20
    console.log(a);//20
}

作用域

作用域有全局作用域和函数作用域,我的理解其实就是变量(标识符)作用域,当执行一段代码时会在所在作用域解析变量和函数,该作用域变量会覆盖外围的作用域,该作用域找不到的标识符会沿着作用域链向上延伸查找,找不到就报错;

var a = 10;
!function(){
    console.log(a);
    var a= 20;
    console.log(a);
}();
console.log(a);
//上面理解为
var a = 10;//全局作用域
!function(){
    var a;//函数作用域
    console.log(a);//undefined,a向上查看-》函数作用域-》全局作用域
    a= 20;
    console.log(a);//20
}();
console.log(a);//10,查找到全局作用域,函数作用域不可见

再来看一个例子

console.log(b)
if('b' in window){
    var b=10;
    console.log(b);
};
console.log(b);
//ES3,ES5中if..else,with,while,for,switch等等是没有作用域的,
//只有全局作用域和函数作用域,如下
var b;
console.log(b);//undefined
if('b' in window){
    b =10;
    console.log(b);//10
};
console.log(b);//10

闭包

每个人都有不同理解,我的理解是闭包就是让函数闭不了包,外部变量的值被缓存,内部变量可访问外部变量,也可以说是外部变量可访问内部变量,用法不同说法就不同,闭包在一些简单例子上可以代替new实例化的开销,用自执行函数先把该执行的执行完

var Fn =(function(){
    var obj = {};
    obj.id=10;
    return {
        obj:obj
    };
})();

来看一个最常用的淘宝tab栏切换例子

<ul>
    <li>素颜</li>
    <li>断桥残雪</li>
    <li>最佳歌手</li>
</ul>

var tab = document.querySelectorAll('ul>li');
for(var a=0;a<tab.length;a++){
    tab[a].onclick=function(){
    console.log(a);
    }
};
//当点击tab每一项时候,我们会很惊奇地发现打印出来的都是tab.length,
//由于for循环的时候a不会进去函数里面,等循环结束后,当你点击都已变为
//tab.length,我们需要把a缓存起来

第一种

for(var a=0;a<tab.length;a++){
    tab[a].index=a;
    tab[a].onclick=function(){
    	console.log(this.index);
    }
};
console.log(a);//tab.length,这个下面第三种方法会说
//这种是传统做法,把一个自定义属性绑定在每个dom节点li上,增大dom的开销

第二种

for(var a=0;a<tab.length;a++){
    !function(a){
        tab[a].onclick=function(){
        	console.log(a);
        }
    }(a);
};
//或者
for(var a=0;a<tab.length;a++){
    tab[a].onclick=(function(a){
    	return function(){
    	    console.log(a);
        }
    })(a);
};
console.log(a);//tab.length
//这种是闭包写法,使用自执行函数缓存起每个a,当然闭包也会有内存泄漏,
//性能等其它问题,用的适当还是可以的

第三种

for(let a=0;a<tab.length;a++){
    tab[a].onclick=function(){
        console.log(a);
    };
};
console.log(a);//a is not defined
//这是es6的写法,一个let搞定所有问题,原生的方法,推荐使用;
//大家发现第二个a打印'a is not defined',
//但是第一种和第二种方法就可以访问到a为tab.length,这有可能导致变量
//泄漏或者冲突,let就很好的解决了这个问题,那是因为es6新增
//了块级作用域,外部访问不到for的作用域

this

this指向执行时所在的作用域,一般为window和函数,node环境为global, 来看个例子就明白了

var id=10;
var obj  = {
    id:100,
    show:function(){
        console.log(this.id);
        console.log(this.name);
    }
}
var b = obj.show;
b();
obj.show();

//obj.show先保存起来后在调用时,这时是直接调用一个函数b,函数的this
//指向window,注意window.name是window默认就有的为空,结果就是10 ''
//obj.show()方式是直接调用,this对象指向obj,name为undefined,
//结果就是100 undefined

this中的bind,call,apply

bind,call,apply可以改变当前函数的作用域,bind不一定要立即执行函数,call,apply必须立即执行函数 来看一个构造函数继承的例子

var Father = function(){
    this.name='张三';
    this.age=50;
    //以下三种都可以
    var son = Son.bind(this);//指向的对象
    son('葫芦娃',10);//间接调用,也可以直接调用
    //Son.apply(this,['葫芦娃',10]);//第一个参数是指向的对象,第二个参数是数组
    //Son.call(this,'葫芦娃',10);//第一个参数是指向的对象,后面分开写
}
Father.prototype.show=function(){
    console.log('爸爸叫'+this.name);
}
var Son = function(name1,age1){
    this.name1=name1;
    this.age1=age1;
}
var father = new Father;//通过改变Son构造函数的this指向为Father
console.log(father);//{name: "张三", age: 50, name1: "葫芦娃", age1: 10}

来看个bind的例子

var obj = {};
obj.show = function () {
    function _show() {
        console.log(this)
    };
    return _show.bind(obj);
}();
obj.show();

// 打印obj对象,由于先声明赋值了,自执行函数后,函数_show的this
//指向被bind方法改为obj

var obj = {
    show: function () {
        function _show() {
            console.log(this)
        };
        return _show.bind(obj);
    }()
  };
  obj.show();
  
 //打印window对象,没事先声明赋值,这里obj变量提升,自执行函数后,
 //bind里面的obj为undefined,this为undefined的默认指向window

箭头函数:默认指向所在的宿主对象,也就是上一级对象,而不是执行时的对象,基于这个this指向上一级的特殊性,我们在某些情况下就不需要缓存this的值,直接使用;

var obj = {
    id:100,
    show:function(){
        (()=>{
        	console.log(this)
        })();
        setTimeout(()=>{
        	console.log(this)
        },1);
    },
    show1:()=>{
        console.log(this);
    }
};
var obj1 = obj.show;.
obj1();//window,箭头函数上一层是个普通函数,普通函数this指向window
obj.show();//obj,箭头函数上一层作用域的this指向obj
obj.show1();//window,this指向上一级即window

箭头函数与普通函数的混合嵌套

var obj = {
    show:function(){
    	setTimeout(fn);
    	function fn(){
    	    console.log(this);
    	    setTimeout(()=>{
                    console.log(this);
                    setTimeout(()=>{
                         console.log(this);
                    })
            })
	};
    },
    show1:function(){
        setTimeout(fn.bind(obj));
        function fn(){
            console.log(this);
        	setTimeout(()=>{
        		console.log(this);
        		setTimeout(()=>{
        			console.log(this);
        		})
        	})
        };
    }
};
obj.show();//都是window,最外面的定时器是普通函数,普通函数this指向window,每个箭头函数this指向上一层
obj.show1();//都是obj,最外面的定时器this指向被改变为obj,每个箭头函数this指向上一层

综合题

第一道

var a = 1;
!function(){
    var a = 10;
    function fn(){
    	this.a += this.a;
    	a+=a;
    	console.log(this.a);
    	console.log(a);
    };
    var obj = {
    	a:5,
    	show:fn
    };
    obj.show();
    var obj1 = obj.show;
    obj1();
}();
//这里最主要是考查this指向,去年面试时候做到的笔试题,自己加以改进
//'obj.show()'直接调用,此时函数fn的this指向obj,
//this.a就是5,this.a累加后就是10,根据就近原则,变量a会
//沿着作用域链向上查找,找到上一层的10就停止了,
//a累加后就是20
//'var obj1 = obj.show;'这步保存起普通函数,普通函数被调用时
//this指向就是window//,this.a===window.a就是1,累加后就是2,变量a依然
//会沿着作用域链向上查找,找到上一层的是20,因为上面已经被累加了一次,
//这是一个坑,很容易忘记,上下两次调用是会互相影响的,a=20在累加就是40

//答案为:
//obj.show();//10 20
//var obj1 = obj.show;
//obj1();//2 40

第二道(闭包的经典题目,原题奉上)

function fun(n,o) {
    console.log(o);
    return {
        fun:function(m){
          return fun(m,n);
        }
    };
};
//写出a,b,c的运行结果
//var a = fun(0); a.fun(1); a.fun(2); a.fun(3);
//var b = fun(0).fun(1).fun(2).fun(3);
//var c = fun(0).fun(1); c.fun(2); c.fun(3);
//这里就是考查闭包的层层嵌套与多次回调,之前面试遇到的,
//当时做的时候有点紧张,不过后面自己运行后发现做的还是正确的
//var a = fun(0); a.fun(1); a.fun(2); a.fun(3);
//第一个a保存fun(0)的运行结果即return里的对象,由于o没传参数,
//打印undefined,参数n=0被保存在当前作用域中,不会被销毁,
//供当前作用域链及以下使用,这是闭包的精髓,后面回调函数传参也是如此,
//这个a.fun(1)就是调用fun(0)的结果,传参m=1,执行返回fun(m=1,n=0)
//在执行回调fun(n=1,o=0),console.log(o)就是0,后面的a.fun(2)和
//a.fun(3)也是如此,打印都是0;

//var b = fun(0).fun(1).fun(2).fun(3);
//fun(0).fun(1)这步其实就是前面说的,
//fun(0).fun(1)的返回就是return的对象,.fun(2)在调用改对象并且
//传参m=2,返回fum(m=2,n=1)在执行回调fun(n=2,o=1)
//打印console.log(o)就是1,并且返回return对象,
//在.fun(3)在调用改对象并且传参m=3,返回fum(m=3,n=2)
//在执行回调fun(n=3,o=2)打印console.log(o)就是2

//var c = fun(0).fun(1); c.fun(2); c.fun(3);
//前面两个理解了,这个也不会有问题,就不多做解释了

//var a = fun(0); a.fun(1); a.fun(2); a.fun(3);//undefined 0 0 0
//var b = fun(0).fun(1).fun(2).fun(3);//undefined 0 1 2
//var c = fun(0).fun(1); c.fun(2); c.fun(3);//undefined 0 1 1

写到这里终于结束啦,js里面还有很多奇淫技巧,每次看一篇好文或者一本书都会被新的视角冲击到,前方高能,还需继续踩坑,有什么需要交流指正的请留言呀!