JS 预解析机制与定义函数的三种方式
1、JS 预解析机制与定义函数的三种方式
定义函数的三种方式
1、 函数声明(用的最多,推荐优先使用)
function fn(){ } fn()//直接调用
2、 函数表达式(也叫匿名函数)
var fn=function([参数]){ //把函数赋给一个变量,函数并没有真正的名字,所以叫匿名函数 } fn()
3、 自执行函数 (自己执行自己,并且在声明的同时就调用自己,只能调用一次)
(1)第一种写法
(function(){ console.log('自执行函数') })()
这样写的原理是:他是由函数表达式演变过来的,var fn = function(){} fn(); 把 fn 替换成:
function(){},就成了: function(){}(); 为了保持整体性,在 function 加上一个(),所以就变成
了 (function([这里是形参]){})([这里是实参])
(2)第二种写法
(function(){ console.log('自执行函数') }())
值得注意的是,用这种写法会有一个小漏洞,看个例子:
var fn =function(){ console.log('123') } (function(){ console.log('456') }())
输出结果是:
456
123
这里匿名函数 fn 明明没调用,为什么会输出?看下面简化后的例子:
<script> var fn = function(){ console.log(123); }(function(){ console.log(456) }()) </script>
可以很明显的看到(function(){console.log(456);}())这个自执行函数调用输出了 456,同
时,这个自执行函数跟在匿名函数的后面,JS 解析的时候把这个匿名函数也解析成为了自
执行函数,所以就输出了 123,解决的办法很简单,在第一个匿名函数的后面加一个分号“;”
或者直接采取第一种写法就能避免这种问题
JS 预解析机制
在当前作用域下,js 运行之前,会把带有 var 和 function 关键字的事先声明,并
在内存中安排好。(这个过程也可以理解为变量提升)然后再从上到下执行 js
语句。预解析只会发生在通过 var 声明的变量和 function 声明的函数上。
1、预解析过程:
(1)把变量的声明提升到当前作用域的最前面,只会提升声明,不会提升赋值。
(2)把函数的声明提升到当前作用域的最前面,只会提升声明,不会提升调用。
(3)先提升 var,再提升 function
因此,我们在定义函数时,绝大多数都使用第一种方式(函数声明的方式)
var
使用 var 定义的变量预解析:告诉解析器知道有这个名字的存在并默认将该变量
赋值 undefined ,如下:
console.log(x) //undefined var x=15
变量 x 虽然是在 console.log 后面定义的,但是使用 var 声明的 x 会提前保存在
内存中,并赋值 undefined ,然后再从上往下执行 js 语句 。它的执行顺序类
似于如下:
var x console.log(x) //undefined x=15
先声明了 x ,x 没有赋值,默认赋值为 undefined ,输出的结果自然为
undefined 。然后再给 x 赋值为 15。
需要注意的是,如果变量声明没有使用 var ,不存在变量提升的。如下:
x 没有使用 var 声明,所以报错找不到 x。
console.log(x) //error :x is not defined x=15
当匿名函数多用于定时器里面和注册事件的时候,比如
<script> btn.onclick = function(){ console.log("这是一个匿名函数") } </script>
2、预解析示例:
<script> var num=789 function fn(){ console.log(num) var num =789 } fn();//调用函数 </script>
此时控制台打印的是 undefined,原因是:JS 解析代码时,把函数的声明还有变量的声明
提升到当前作用域的最前面,所以代码就变成:
<script> var num; num=789 function fn(){ var num;//从这里可以看出num只是申明而没有定义赋值 console.log(num) num =789 } fn();//调用函数 </script>
2.、逐行解读代码
声明变量如果没有赋值,则js自动赋值undefined
var a; =>等同于var a=undefined
然后查找function,找到函数,直接拿过来(也就是说在预解析的时候,函数就已经声明了)
在逐行解读代码时,var有值的就赋值,函数可以直接跳过。
预解析问题
1、变量名冲突 预解析都是undefined,只有解读代码时会覆盖
2、变量名和函数名冲突,变量名舍弃,保留函数名
3、函数名冲突,谁在后面就留谁
4、不要在代码块里定义函数
5、预解析时,函数里面带有参数的时候,对参数不使用var,那么它只是局部变量
2、变量
基本类型 | 引用类型 |
不可修改 | 可以修改 |
保存在栈内存中,固定大小空间 | 保存在堆内存中,大小空间不固定 |
按值访问 | 按引用访问 |
比较时,值相等即相等 | 比较时,同一引用才相等 |
复制时,创建一个副本 | 复制的其实是指针(指向同一个对象) |
按值传递参数 | 按值传递参数 |
用typeof检测类型 | 用instanceof检测类型 |
字符串方法都不是在原来基础修改,都会返回一个新值
基本类型使用方法都会找到对应的包装对象,包装对象的属性借给基本类型,这样就能修改基本类型的值
比如1->number 'aac'->string
引用类型都是在原来基础修改
引用类型的三种比较方法
第一种方法:第一个引用的值赋值给第二个引用,这才算同一个引用
<script> var xm= { age:18, score:4 }; var xh=xm; console.log(xm === xh) </script>
第二种方法:通过遍历或者循环比较每一项,如果每一项都相等,那就是相等的引用类型
<script> var xm= { age:18, score:4 }; var xn= { age:18, score:4 }; function equaObj(a,b){ for(var p in a){ if(a[p] !==b[p])return false } return ture } console.log(equaObj(xm,xn)) </script>
第三种方法,数组的比较
<script> var xm= { age:18, score:4 }; var xn= { age:18, score:4 }; function equaArray(a,b){ if(a.length !==b.length)return false; for(var i=0;i<a.length;i++){ if(a[i] !==b[i])return false } return ture } console.log(equaObj(xm,xn)) </script>
引用类型的复制
因为指向的是同一个引用,所以改变新的引用旧的也会发生改变,
1、创建一个函数,对原引用类型进行循环遍历,输出得到的就是独立的一样的引用类型(浅拷贝)
2、递归函数,递归原引用类型所有值给新的引用类型,就是深拷贝
如果能将原引用类型的对象也能拷贝,就叫深拷贝,
---à
2、形参和实参
形参和实参个数,
当形参等于实参时,也是最常见的方法
当形参大于实参时,
1.如果有可选参数(默认参数),就可传可不传
jq的$('',document.getElementById(''))
第二个值就是默认值,如果没有则是在上下文取,如果有,就获取在该元素里的标签
2.如果没有默认参数,缺少的用Null或者空值代替
当形参小于实参时,借助arguments来获取参数个数
arguments是类数组,保存的是实参的值,不能使用数组的方法,是每个函数独有的
不要使用arguments随意改变形参,会影响参数的输出
argument的值与形参一一对应,如果改变arguments的值,形参也要改变
arguments.callee指代函数本身(严格模式下失效),在需要变化函数名的函数里面使用函数本身的时候,可以使用该方法。
替代方法是把函数赋值给name,函数本身使用一个小名,这样无论函数怎么变化,函数内部都能调用函数,如下图
var factorial function fn(num){ if(num<=1)return 1; return num*fn(num-1) } console.log(factorial(5)) arguments.length是实参的个数,fn().length是形参个数
引用类型的参数传递。上图的打印是xm,因为在函数setName里,obj={}是另外一个引用类型,和形参obj没有关系,所以输出的是xm
什么可以做参数
基本类型和引用类型
数字,字符串,Null,undefined,布尔值
对象,数组,函数(回调函数,满足某种条件的时候才会调用)
函数的输出 返回值
return 函数中返回函数值
break 循环中,符合条件直接跳出循环
continue 循环中,跳出本次循环接着下次循环
return 后不接值的时候是提前退出函数
alert打印的是()里内容的.string方法
return [],返回的是多个值,类似数组
return 对象,返回对象
作用域
局部作用域速度比全局作用域快
所以jquery就是把window当做参数传给局部作用域
使用withj可以延长作用域链(鸡肋,不推荐使用) with(person){ name:'' } 修改name的值等于修改person.name
3、函数的定义、调用、传参和返回值
函数的定义
字面量 | 构造函数 | 赋值表达式 |
function fn() {} | var fn = new Function(); | var fn = function () {}; |
函数声明 | 赋值语句 | 赋值语句 |
简洁,直观 | 多此一举,效率低下 | 简洁,直观 |
声明提前 | 必须在调用语句之前 | 必须在调用语句之前 |
查找对象的值:cat.name
查找字符串p的值:cat['name']
查找name(随意)的值:cat.name
删除对象的值:delete cat.name
检测对象是否有某个值:'name' in cat,返回boolean值
函数的调用
普通函数 | 匿名函数 | 构造函数 | 方法 | 间接 |
fn() | (function () {})() | new Object() | Obj.method() | fn.call() |
间接调用
每个函数都有call、apply方法
person.getName.call(window),调用的是window下的值。因为call改变了this指向为window,所以调用的是全局的变量。
apply(window),apply为一个参数的时候是和call一样的。
apply(window,[‘’]) ,第二个参数是数组,可以通过数组方式传参,也可以拿来继承函数
函数作为参数传递时,可以使用函数名传递,因为函数名fu就代表函数本体,如果是fu(),函数就执行
递归调用:计算阶乘
方法调用,只能调用合法标识符方法,如果出现不合法的,可以用["]
模拟点击事件document.onclick()
链式调用要在方法里返回this指向,这样才能保证后面的方法可以使用
函数的参数
形参 | 实参 |
函数定义时括号内的参数 | 函数调用时括号内实际传递的参数 |
接收实参的值 | 将值赋值给形参 |
个数为fn_name.length | 个数为arguments.length |
函数的返回值
return | continue | break |
用在函数中 | 用在循环中 | 用在循环中 |
跳出函数 | 跳出本次循环 | 跳出当前循环 |
4、JavaScript 内存管理与垃圾回收机制
为什么要进行垃圾回收?
因为程序中存在很多数据 , 这些数据在内存中占据一定的空间 。在程序运
行中 ,一些没有用的数据(这些数据可以称为垃圾)还会在内存中占据空
间。如果不进行垃圾回收的话,随着程序的运行,可用的内存越来越小 ,
必然会带来程序性能的下降,造成卡、慢甚至系统异常。
js 垃圾回收机制
Javascript 具有自动垃圾回收机制,也就是说,执行环境会负责管理代码执
行过程中使用的内存。
原理:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释
放其内存。
标记清除
js 中最常用的垃圾回收方式就是标记清除。当变量进入环境时,例如,在函
数中声明一个变量,就将这个变量标记为“进入环境”。此时的变量在函数
执行过程中一直存在,直到函数结束后,将变量标记为“离开环境”,变量
就被回收了。(JavaScript 中全局变量的在浏览器卸载页面才会被销)
如下例子:
function count(){ var num=0 num++ console.log(num) } count();//1 count();//1
定义一个函数 count ,函数 count 被调用了两次结果 num 都输出 1 ,说
明当第一次执行之后函数里面的变量 num 会被回收。然后在第二次调用时
又重新声明变量 num 并初始化为 0,再进行计算输出结果 1。
引用计数(不常用)
引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一
个引用类型值赋给该变量时,则这个值的引用次数就是 1。如果同一个值又
被赋给另一个变量,则该值的引用次数加 1。如下:
var obj={ name: 'xiaoming', age:23 };//引用次数为1 var person =obj //引用次数为2
相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用
次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值
了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收器下次再
运行时,它就会释放那些引用次数为 0 的值所占用的内存。如下:
var obj={ name: 'xiaoming', age:23 };//引用次数为1 var person =obj //引用次数为2 person = {} //引用次数减1,剩余引用次数为1 obj = {} //引用次数减1,剩余引用次数为0
Netscape Navigator3 是最早使用引用计数策略的浏览器,但很快它就遇
到一个严重的问题:循环引用。循环引用指的是对象 a 中包含一个指向对象
b 的指针,而对象 b 中也包含一个指向对象 a 的引用。
function fn(){ var a={} //引用次数为1 var b={} //引用次数为1 a.pro=b;//引用次数为2 b.pro=a; //引用次数为2 } fn()
以上代码 a 和 b 的引用次数都是 2,fn()执行完毕后,两个对象都已经离开
环境,在标记清除方式下是没有问题的,但是在引用计数策略下,因为 a 和
b 的引用次数不为 0,所以不会被垃圾回收器回收内存,如果 fn 函数被大
量调用,就会造成内存泄露。在 IE7 与 IE8 上,内存直线上升。
IE 中有一部分对象并不是原生 js 对象。例如,其内存泄露 DOM 和 BOM
中的对象就是使用 C++以 COM 对象的形式实现的,而 COM 对象的垃圾
回收机制采用的就是引用计数策略。因此,即使 IE 的 js 引擎采用标记清除
策略来实现,但 js 访问的 COM 对象依然是基于引用计数策略的。换句话
说,只要在 IE 中涉及 COM 对象,就会存在循环引用的问题。
var obj = {} var elem = document.getElementById("box") elem.someArr = obj obj.someProperty = elem
如上例子中一个 dom 对象和一个原生 js 对象之间循环引用,即使 DOM 从
页面中移除,它也永远不会被回收。最简单的解决办法就是手动解除引用
elem.someArr = null obj.someProperty = null
内存管理
计算机分配给 Web 浏览器的可用内存数量通常要比分配给桌面应用程序的
少。这样做的目的主要是出于安全方面的考虑,目的是防止运行 JavaScript
的网页耗尽全部系统内存而导致系统崩溃。因此,确保占用最少的内存可以
让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行中的代码
只保存必要的数据。一旦数据不再有用,最好通过将其值设置为 null 来释
放其引用——这个 做法叫做解除引用(dereferencing)。如下:
function personObj(name){ var person = new Object(); person.name = name; return person } var student = personObj("sunshine"); console.log(student.name) //手动解除student的引用 student = null
这一做法适用于大多数全局变量和全局对象的属性。局部变量会在它们离开
执行环境时自动被解除引用
PHP断点
print_r();exit()