JavaScript高级知识(一)

作用域

1
2
3
4
5
6
7
8
9
10
11
12
<script>
var a = 2;
function t1(){
var b = 3;
function t2(){
var c = 4;
alert(a+b+c);
}
t2();
}
t1();
</script>
  • 在JS中,函数嵌套是非常普遍的,在函数嵌套中,对变量进行如下的寻找方式:首先在函数内寻找,寻找不到,则往函数外层寻找,……,直到全局(window)区域

声明变量var

  • var是在函数运行的上下文中,声明一个变量,如果不加var,则是一个赋值操作,但不要狭隘的理解为–>声明了一个全局变量【见下例】
1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
function t1(){
var a;
function t2(){
a = 2;
b = 4;
}
t2();
}
t1();
console.log(a); //输出undefined
console.log(b); //输出2
</script>
  • 以window.xxx引用全局变量,寻找不到,作为某个属性不存在,返回undefined 【window.a】

  • 直接以xxx引用某个变量,寻找不到,则是报xxx is not defined错误的 【a】

1
2
3
4
5
6
7
8
9
<script>                                             <script>                                       
var str1 = "global"; var str1 = "global";
function t1(){ function t1(){
console.log(str1); //global console.log(str1); //global
console.log(str2); // str is not definded console.log(str2); //undefinded
str2 = "local"; var str2 = "local";
} }
t1(); t1();
</script> </script>
  • JS代码自上而下执行,但是,JS代码在整体运行时分为【词法分析期】和【运行期】两部分

  • 自上而下执行之前,先有一个【词法分析】过程,以上面的结果为例:

1
2
3
4
5
6
7
8
9
第一步:分析t1()
t1(){
var str2; //分析出t1内有str2局部变量,注意此时函数未执行,因此str2的值是undefined
}

第一步:执行t1()
console.log(str1); //global
console.log(str2); //undefinded
str2 = "local"; //此时str2的值为local

词法分析

语法分析,分析3样东西:

  • 第一步:先分析参数

  • 第二步:在分析变量声明

  • 第三步:分析函数声明

一个函数能使用的局部变量,就从上面的3步分析而来

具体步骤:

  • 第一步:函数运行前的一瞬间,生成Active Object(活动对象),下称AO

  • 第二步:把函数声明的参数,形成AO的属性,值全是undefined,如果有实参,则接收,并形成AO相应的属性的值

  • 第三步:分析变量声明!如var age,如果AO上还没有age属性,则添加AO属性,值是undefined;如果AO上已经有age属性,则不做任何影响

  • 第四步:分析函数声明,如 function foo(){ },则把函数赋给AO.foo属性,如果此前foo属性已经存在,则被无情的覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<script>
function t3(great){
var great = "hello";
alert(great);
function great(){
}
alert(great);
}
t3(null); //输出 hello hello
</script>

/*
分析过程:
1.形成AO = {}
2.分析形参,AO = {great:undefined},接收实参,AO = {b:null}
3.分析great变量声明,发现AO已有age属性,不做任何影响
4.分析great函数声明,AO.great = function great(){},被覆盖成函数

执行过程:
great = "hello";
alert(great);
alert(great);
*/

<script>
function a(b){
alert(b);
function b(){
alert(b);
}
b();
}
a(1);
</script>

/*
分析过程:
1.形成AO = {}
2.分析形参,AO = {b:undefined},接收实参,AO = {b:1}
3.分析var变量声明,此函数没有var
4.分析函数声明,AO = {b:function b(){alert(b);}},被覆盖成函数

执行过程:
alert(b); //function
b(); //由作用域寻找到a函数中的b,即function
*/

<script>
function a(b){
alert(b);
b = function (){
alert(b);
}
b();
}
a(1);
</script>

/*
分析过程:
1.形成AO = {}
2.分析形参,AO = {b:undefined},接收实参,AO = {b:1}
3.分析var变量声明,此函数没有var
4.分析函数声明,没有!b = function(){},是一个赋值,在执行期才有用

执行过程:
alert(b); // 1
b = function (){
alert(b);
}
b(); //由作用域寻找到a函数中的b,即function
*/

函数声明与函数表达式

  • 函数可以赋值给变量,也可以作为参数来传递

  • JS被称为披着C外衣的lisp语言,lisp是一种强大的函数式语言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function t1(){}

t2 = function(){}

/*
这两种方式效果是不同的:
t1是函数声明,虽然全局内也得到一个t1变量,值是function
而t2只是一个赋值过程,值就是右侧表达式的返回结果,即函数

因此t1,t2两种方式在词法分析时,有着本质去区别:
前者在词法分析阶段就发挥作用,而后者,在运行阶段才发挥作用
*/

(function(window,undefined){}) //内层表达式,返回值是函数,包在小括号里,当成表达式来执行
(function(window,undefined){})(window) //立即调用

/*
内层函数没有起名字,称为匿名函数,这种手法,匿名函数,立即执行,不污染全局,称为立即执行匿名函数表达式,
在第二个括号里面传入window,是为了我提升内部查找变量的速度,不传undefined,是为了安全,因为在低版本IE和火狐中,
undefined可以被重新赋值,如undefined = 3,声明undefined局部变量(名字是undefined而已),
同时,又不传参,值自然是undefined,防止了外界对undefined的污染
*/

作用域链

QQ20171222-141204.png

argumengs详解

  • 是一个长得很像数组的对象

  • 内容是函数运行时的实参列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//arguments收集“所有”的实参,即使没有与之相对应的形参
(function(d,e,f){
//在此函数内,无法用d,e,f形参来取得“haha”,因为没有与之相应的形参,但我们可以用arguments来获取任意多个实参
console.log(arguments[3]);
arguments[3] = "china";
console.log(arguments[d]); //输出 china,形参与对应的arguments单元,其实是相互映射的,互相影响
})("hello","world","!","haha")

//arguments可以获取函数运行时,收到的实参个数(在多态里可以用到)
(function(d,e,f){
console.log(arguments.length); //输出 3
})("hello","world","!","haha")

//arguments.callee属性代表“当前运行的函数”

(function(){ //不用函数名,匿名函数,立即执行,完成递归
if(n <= 1){
return 1;
}else{
return n + arguments.callee(n-1);
}
})(100);

函数运行期内,有三个关键的对象:

  • AO ——> 本函数AO上没有某属性,则继续去外层函数的AO上找,直到全局对象,叫做 作用域链

  • arguments ——> 每个函数都有自己的callee,但不会向外层函数接着找arguments的相关属性,即不形成链

  • this ——> 也不形成链

this详解

  • 在JS中函数有4种调用方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
alert(window.x);    //输出 undefined
function t(){
this.x = 333;
}
t();
alert(window.x); // 输出 333

/*
作为普通函数来调用时,this的指向window,准确的说,this为null,但被解释成window,但在ECMASCRIPT标准中,
如果this为null则解释成undefined
*/

//作为对象的方法来调用,this指向方法的调用者,即该对象
var obj = {xx:999,yy:888,t:function(){alert(this.xx);}};
obj.t(); //输出 99
var dog = {xx:"wangwang"};
dog.t = obj.t;
dog.t() //输出 wangwang

/*
作为方法调用时,this指向其调用那一刻的调用者,即母体对象,不管被调用函数,声明时属于方法还是函数,
*/

//函数作为构造函数调用时
//JS中没有类的概念,创建对象是用构造函数来完成,或直接用json格式来写对象
function Dog (name,age){
this.name = name;
this.age = age;
this.bark = function(){
alert('I am ' + this.name + '!');
};
}
var dog = new Dog('huzi',2);
dog.bark();

/*
new Dog发生了以下几个步骤:
1.系统创建空对象{},(空对象construcor属性指向Dog函数,先别管)
2.把函数的this ——> 指向该空对象
3.执行该函数
4.返回该对象
*/

function Pig(){
this.age = 99;
return 'abc';
}
var pig = new Pig(); //返回pig对象,因为函数作为构造函数运行时,return的值是忽略的,还是返回对象

//函数通过call,apply调用
//语法格式:函数.call(对象,参数1,参数2,……,参数N);
function t (num){
alert('我的真实年龄是'+ this.age); //输出 我的真实年龄是28
alert('但我告诉别人是'+ (this.age+num)); //输出 但我告诉别人是18
}
var human = {name:'lisi',age:28};
human.t = t;
human.t(-10); //this指向了human,但human多了一个方法

//接下来我们不把t赋为human的属性,也能把this指向human
var wangwu = {name:'wangwu',age:30};
t.call('wangwu',5); //输出 我的真实年龄是30 但我告诉别人是35

/*
解释fn.call(对象,参数1,参数2,……,参数N);
运行如下:
1.fn函数中的this ——> 指向对象obj
2.运行fn(对象,参数1,参数2,……,参数N);
*/

闭包

image

  • 在大部分语言中,t1被调用执行,则申请内存并把其局部变量push入栈。t1函数执行完毕,内部的局部变量,随着函数的退出而销毁,因此age = 20的局部变量已经消息。

  • 但是在JS中,age = 20这个变量,却被t2捕捉,即使t1执行完毕,通过t2,依然能访问该变量

  • 像这种情况:返回的函数,并非孤立的函数,甚至把其周围的变量环境,形成了一封闭的“环境包”,共同返回,所以叫闭包。

  • 一句话概括:函数的作用域取决于声明时,而不取决于调用时!