引言
刚刚学完js之后我感觉JavaScript并没有什么东西,好像也不是很难。但是自己运用的时候发现自己学的并不扎实,里面的知识点还是挺多的,甚至有些理解了我还是会出错。所以为了打好基础,我会复习回顾JavaScript的知识点,做好记录,记录是我自己的学习反馈,也是同大家的学习交流,找到自己的不足。这个小记录总结了js中基础且重要的五个问题,看完后能够深入的理解预编译中变量被提升,闭包的作用以及闭包触发哪些问题等知识点。
我会从深度和广度两个方面来叙说。
文章的开始
js语言有两个特点,一个是单线程语言,一个是解释性语言(读一句执行一句)。
知识点1:预编译
1.js执行三步曲
语法分析:通篇分析,通篇扫描一遍看有没有低级错误不进行编译
预编译: 函数整体提升,变量的声明提升
解释执行:解释一行执行一行
ps:定义变量包括变量声明和变量赋值,
任何未经声明就赋值的变量归window所有,window就是全局的域。
一切声明的全局变量非局部变量,就都是window的属性
2.函数中的预编译
预编译发生在函数执行的前一步
1.创建AO对象,AO执行期上下文
2.找出形参和变量声明放到AO{}里面,且值为undefined
3.实参和形参相统一
4.找出函数里面的函数声明并且函数体为值
然后在AO里面拿东西,写的东西是给电脑看的,想拿东西要到电脑里面定义好的地方去拿。而不是视觉上的效果
来看一个栗子
function fn(a) {
console.log(a); //function a() {}
var a = 123;
console.log(a); //123
function a() {}
console.log(a); //123
var b = function() {}
console.log(b); //function() {}
function d() {}
console.log(d); //function d() {}
}
fn(1);
复制代码
这道题包括形参名,函数名,变量名都一样的情况,所以我们不能根据自己看到的去判断,可是看在编译之前生成的执行期上下文。
2.1第一步:创建AO对象
AO{
}
2.2第二步:找出变量声明和形参,值为undefined
AO{
形参a:undefined
变量声明b:unfined
复制代码
}
其中有一个形参a,后边的 var a = 123中有又有一个变量a的声明因为前边有一个形参a所以不用重复写。
2.3第三步:形参实参相统一
AO {
a:1;
变量声明b:unfined
复制代码
}
2.4第四步:找出函数里面的函数声明并且函数体为其值
a,d为函数声明,b为函数表达式
AO {
a:function a() {}
变量声明b:unfined
d:function d() {}
复制代码
}
这a的值由1变成function a(){},是因为a是一个函数声明,所以a的值由1变成了函数体。此时预编译的过程完成。然后再进行编译,解释一行执行一行。预编译过程中执行的代码在编译的时候就不会再执行,如var a = 123; 在执行的过程中就不会再执行var a,变量声明被提升优先执行了。所以直接执行a = 123.
3.全局中的预编译
全局里的预编译与函数里面的预编译类似,第一步创建GO对象,第二步找出变量声明,没有第三步实参和形参相统一。下面结合具体的例子来看看
3.1暗示全局变量
function test (){
var a = b = 124;
console.log(window.a); //undefined
console.log(window.b); //124
}
复制代码
上面的变量b是没有经过声明就赋值了124,所以归window所
4.预编译的例子
4.1恶心的预编译0
var x =1; var y = z = 0;
function add(n) {
return n = n+1;
}
y = add(x);
function add(n) {
return n =n + 3;
}
z = add(x)
复制代码
请输出x,y,z的值。最后结果应该x为1,y和z都是4。因为在预编译的时候同名的函数会被覆盖,而不是以我们的直观感觉来判断。因此执行所以1,2,4的结果是错误的。
4.1恶心的预编译1
//GO{
// test:function test(){};
// a:undefined;
// c:234;
// }
function test (){
console.log(b); //undefined
if(a) {
var b = 100;
}
console.log(b); //undefined
c = 234;
console.log(c);// 234
}
var a;
// AO{
// b:undefined
// }
test();
a= 10;
console.log(c);//234
复制代码
在没有执行test()之前
首先生成GO。window === GO
GO{
test:function test(){};
a:undefined;
}
执行test生成
AO{
b:undefined
}
在test函数中,有一个没有变量声明就赋值的变量c,所以应该归全局所有于是
GO{
test:function test(){};
a:undefined;
c:234;
}
4.2恶心的预编译2
//GO {
global: undifined
fn: function fn(){}
//}
global = 100;
function fn() {
console.log(global);
global = 200;
conaole.log(global);
var global = 300;
}
// AO {
// global:undefined
// }
fn();
var global;
复制代码
首先生成GO,然后再生成AO.第一个打印的global因为fn函数里面就有声明变量global,不需要到GO里面去找。所以第一个打印的就是undefined,不是100,第二个打印global=200.
4.3恶心的预编译3
function bar() {
return foo;
foo = 10;
function foo() {
}
var foo = 11;
}
console.log(bar());//function foo(){}
复制代码
首先console.(bar())就是打印出bar的结果,也就是我们要知道返回的foo的值。根据预编译四部曲我们知道函数声明的优先级最高,所以foo:function foo(){}
console.log(bar());
function bar() {
foo = 10;
function foo(){
}
var foo = 11;
return foo;
}
复制代码
在最后对foo赋值11,所以打印出来不是函数声明而是11.可能有些掘友会问foo是一个局部变量,为什么可以在全局里面输出呢?那是因为全局里访问的是全局里面的bar函数,foo是函数里面的一个变量。
知识点2:作用域链
作用域链与原型链不同,作用域链[[scope]],这里面scope的第一位存放GO,第二位存放AO。作用域链是执行期上下文的集合。
1.执行期上下文:
函数被执行时会创建一个执行期上下文,一个执行期上下文定义了一个函数执行时的环境,函数每次执行时的执行期上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行期上下文,函数每次执行时,都会把新生成的执行期上下文填充到作用域链的最顶端。函数执行完毕,执行期上下文被销毁。
2.如何查找变量
从作用域链顶端依次向下查找。
function a() {
function b() {
function c() {}
c();
}
b();
}
a();
复制代码
这个例子里面有abc三个函数,函数定义的时候产生GO,函数执行前产生GO.作用域链如下
a defined a.[[scope]] ---> 0:GO
a doing a.[[scope]] ---> 0: aAO //aAO表示a的AO
1: GO
b defined b.[[scope]] ---> 0: aAO
1: GO
b doing b.[[scope]] ---> 0: bAO
1: aAO
2: GO
c defined c.[[scope]] ---> 0: bAO
1: aAO
2: GO
c doing c.[[scope]] ---> 0: cAO
1: bAO
2: aAO
3: GO
复制代码
知识点3立即执行函数
1.立即执行函数执行完就可以被销毁,节省空间针对初始化功能的函数
//(function () {})()
//(function (){}()) //w3c 建议的方法
复制代码
上面的括号就是将函数声明变成了函数表达式才可以被执行,至于为什么不写函数名是因为立即函数执行完后函数名被销毁,这两个特点你可能还是不明白,不要着急,看完了立即函数的2,3两点再回来看第一条就明白了。
1.立即执行函数的深入理解
只有函数表达式才能被立即执行符号执行
function test(a, b, c) {
var a = 123;
}(1, 2, 3)
复制代码
最后控制台会报错,而且是低级的语法解析错误。截图如下。因为只有表达式才能够被执行符号()执行。而它只是函数声明所以不能被执行。
如:123为数字表达式,1233+234也叫表达式,不是说只有等于号才叫表达式。
1.1关于=的正确理解
var test = function test() {}应该拆分成var test 和 = function test() {}两部分来理解,前面的是变量声明,后面的由于带有等号所以是函数表达式(变量赋值)。
还有一种函数表达式如果被执行的时候是不会报错的,如:
var test = function test() {
var a = 123;
console.log(a);
}()
复制代码
与此类似,正负号感叹号等可以将函数声明变成函数表达式,乘号和除号不可以。
注意
3.但是出现了一个有趣的现象,test函数被执行完之后再找不到test函数了,相当于立即执行函数。
4.一个神奇的例子(阿里巴巴考试题)
function test(a, b, c) {
console.log(a + b + c);
}(1, 2, 3);
复制代码
不会报错,也不会被执行。按道理是要报错的,但是语法解析将(1, 2,3);看成了函数表达式。
知识点4闭包
内部的函数被保存到外部形成闭包。
闭包的概念 : 一个作用域可以访问另一个函数内部的变量
闭包的作用: 延长变量的作用范围
1.什么是闭包
比如下面的例子里面,a函数里面嵌套了b函数,a函数定义和执行之前生成GO和AO,b函数在定义的时候拿到了a的执行期上下文,并且返回了b函数,那么它的执行期上下文也一同被带回到函数外部。a函数执行完后它的执行期上下文被销毁。外部拿到了被销毁函数a的执行期上下文。
function a() {
var num = 100;
function b() {
num ++;
console.log(num);
}
return b;
}
var demo = a();
demo(); //101
demo(); //102
复制代码
第一次执行demo输出101;b函数被返回,第一次demo执行===b(),在b函数执行前会生成b的AO放在作用域链的第0位,里面没有num,所以王作用域链的第一位也就是a的AO里面找,恰好找到了num=100,num++使aAO{num:101},所以第一次输出num的值是101。然后销毁b的AO
第二次执行demo函数输出102,第二次执行demo也就是执行b函数,b定义的时候拿到的是a的执行期上下文,拿到的是a的劳动成果。b执行之前再次生成b的AO,没有找到mum变量,于是就去a的AO里面寻找,此时aAO{num:101},num ++后,aAO{num:102},然后b执行完后销毁AO。
2.闭包的作用
记得刚开始学习闭包,学完了前男朋友去接我的时候问我学了什么,我说闭包。然后他问我闭包的作用我半天答不上来哈哈哈哈,很多初学者刚刚学完之后都是模棱两可。现在就将闭包的作用进行一次归纳总结。
1.实现共有变量
我们来看一个累加器的例子
function add(){
var count = 0;
function demo(){
count ++;
console.log(count);
}
return demo;
}
var counter = add();
counter();
counter();
counter();
counter();
复制代码
解析
在这个例子里面,add函数里面的demo函数被返回到外部,也就是add函数执行完被销毁的时候,count变量的值却被带到了外部,然后执行一次counter后count的值就加1,实现了累加器的功能。counter调一次就加一次。
2.可以做缓存
闭包可以当一个隐式存储结构,我们来看一个吃香蕉的例子
function eater() {
var food = "";
var obj{
eat : function() {
console.log("i am eating" + food);
food = "";
}
push : function() {
food = myFood;
}
return obj;
}
}
var eater1 = eater();
eater1.push("香蕉");
eater1.eat()// i am eating 香蕉
复制代码
解析
这里面返回来了一个obj对象,该对象里面有两个函数也一起被返回保存到外部,food这个变量可以push函数使用,也可以eat函数使用。两个函数都可以对它的值进行操作,所以说food相当于一个隐式的存储体。
3.可以实现封装,属性私有化
function Deng(name, wife){
var preparewife = 'xiaozhang';
this.name = name;
this.wife = wife;
this.divorce = function() {
this.wife = prerarewife;
}
this.saywife = function() {
console.log(preparewife );
}
}
var deng = new Deng()
复制代码
当控制台访问deng.preparewif的时候,返回结果undefined,preparewife 本来应该是随着函数执行完的执行期上下文销毁而销毁,但是divorce和saywife函数中都有用到preparewife 这个变量,随着this一起被返回,也就是闭包的作用之一—实现封装,实现属性·私有化。所以将隐式,私有,无关,附加的东西放到闭包里面。
4.模块化开发中防止局部污染变量
js作用域
作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。
我们都知道js中作用域分为全局作用域和局部作用域,局部作用域也是函数作用域。
其他的什么块儿啊、神马文件啊统统都认为是一个作用域,有时候因为一些重名问题导致的错误让人莫名其妙,难以调试解决。
命名空间解决变量名冲突
后来我们把变量函数都放到命名空间中去,命名空间就是对象,为了防止变量名不冲突,用一个对象将这些变量都封装起来。
var obj = {
depart1:{
chen:{
name:'ddd'
},
zhao: {
name: 'fff'
}
},
depart2:{
ling:{
name:''
},
liao: {
neme:''
}
}
}
var linglong = obj.depart1.chen;
chen.name;
复制代码
上面代码可以将不同的部门的员工给封装起来,比如他们的姓名属性不会冲突。这个解决办法就是命名空间。
闭包解决变量名冲突
回到这里应该如何用闭包防止变量冲突问题。
var name = 'linglong';
var init = (function () {
var name = 'xiaoqin';
function callName () {
console.log(name);
}
return fuction (
callname();
) {}
}())
init();
复制代码
我们来看这个代码,init拿到立即执行函数返回的结果后再执行init函数.就相当于执行callname函数,最后结果是xiaoqin,不是linglong,是因为闭包的原因。
我们来仔细分析,全局中执行callname的时后拿到了立即函数中的name属性作为返回结果,如果立即执行函数中没定义name属性才会获取全局中的name,所以最后的结果是linglong。
总结:我们可以通过在一个函数内定义(这里的立即执行函数)一些复杂的功能,然后定义一个接口(函数)并返回给init,当调用init的时候就会启动这个接口实现功能。并且不污染全局变量。
我们把上面的代码扩展,看看结果是什么
var name = 'linglong';
var init = (function () {
var name = 'xiaoqin';
function callName () {
console.log(name);
}
return fuction (
callname();
) {}
}())
init();
var init2 = (function () {
var name = 'sicheng';
function callName () {
console.log(name);
}
return fuction (
callname();
) {}
}())
init2(); //sicheng
复制代码
复习一下,在开发的过程中,不同的人负责不同的功能,但是如果都使用了相同的变量,变量名会产生冲突,为了防止变量名不冲突,讲了两种方法。
虽然现在webpack、gulp等打包工具来解决,但是js是基础,掌握好js学后面才不会吃力。
5.内存泄露
内存泄露就不多说啦,就是闭包内的东西返回到全局中,全局能够拿到闭包内的变量。
构造函数原理
调用new的时候
1.在函数体最前面隐式加上this = {}
2.执行this.xxx = xxx
3.隐式返回this
function Person(name, height) {
var that = {};
that.name = name;
that.height = height;
return that;
}
var person = Person('eee',189);
var person = Person('xx', 120)
复制代码
包装类
原始值可以操作属性,隐式三段式.这儿的原始值是string、boolean、number类型。
//包装类
var num = 4;
num.len = 3;
//new Number(4).len = 3; delete
//
//new Number(4).len
console.log(num.len); //undefined
复制代码var str ="abcd";
str.length=2;
console.log(str);//abcd
var arr=[1,2,3,4];
arr.length = 2;
console.log(arr);//[1,2]
复制代码
原始值没有属性和方法的,至于为什么能够调用是经过了一个包装类的过程,自动创建Number的一个对象然后再给予一个len的属性,创建完后又删掉了这个属性,所以最后是undefined。但是数组有length属性,数组length属性可以被修改。
文章的结束
这篇比较长,而且不是同一个时间段写的,不够好的地方请各位看官指出,觉得我的分享有用的话,欢迎大家关注点赞鸭,后面我将会对js中磨人的this指向做一个全面的总结~~~
最后祝祖国生日快乐,永远和平,繁荣昌盛鸭~~~