第一章 函数高级
一、作用域与作用域链
1.作用域
(1)什么是作用域
a.就是一块“地盘”,一个代码所在的区域
b.作用域是静态的(相对于执行上下文对象),在编写代码时就确定了
c.作用域是一套非常严格的规则,这套规则跟变量的查询有关
(2)作用域分类
1.全局作用域:是针对于全局变量来说,(声明在函数外部的变量)
2.函数作用域:使针对于局部变量来说的,(声明在函数内部的变量)在函数中定义的变量在函数外不能被获取
3.没有块作用域(ES6有了)
(3)作用域的作用
隔离变量,不同作用域下同名变量不会有冲突
(4)作用域的定义
1.广义上来说:
作用域是负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
狭义上来说:
2.作用域是程序代码中定义的一个区域,其控制着变量与参数的可见性及生命周期
总结:
作用域与变量的读取跟存储有着密不可分的联系。
2.作用域链
(1)什么是作用域链
1. 多个上下级关系的作用域形成的链, 它的方向是从下向上的(从内到外) 2. 查找变量时就是沿着作用域链来查找的
3. 作用域链的个数=n+1(n表示函数声明的个数)
(2)查找一个变量的规则
1. 在当前作用域下处于活动状态的执行上下文中查找对应的属性, 如果有直接返回, 否则进入上一级 2. 在上一级作用域的执行上下文中查找对应的属性, 如果有直接返回, 否则进入上一级 3. 直到全局作用域, 如果还找不到就抛出找不到的异常
3.左右查询
(1)左查询
等号的左边
在变量所在的作用域链中找不到对应变量的声明,浏览器的JS引擎会主动在全局作用域内声明一个
(2)右查询
等号的非左边
在变量所在的作用域链中找不到对应变量的声明,浏览器引擎会抛出ReferenceError: a is not defined(报错)。
(3)练习
function(){
function test(a){
b=a;
console.log(b);//2 对b进行左查询 当前作用域没有b的声明,在全局var 了一个b 并把a的值给了b
}
test(2);
})()
console.log(b);//2 对b进行右查询,在全局作用域中找到了b的值
console.log(b) ;//报错 找不到变量的声明
function(){ function test(a){ var b=a; console.log(b);//2 } test(2); })() console.log(b);//be is not defined 当前作用域没有b的声明
作用域面试题
<script>
/*
问题: 结果输出多少?
*/
var x = 10;
function fn() {
console.log(x);
}
function show(f) {
var x = 20;
f();
}
show(fn);
</script>
输出:10
先var一个x=10,然后继续往下执行,看到函数的声明跳过,(函数的声明,当函数被调用时才会执行),
show(fn),对function show调用,传入实参fn,然后对function fn 进行调用,对x进行右查询,找到变量X的值为10。
var fn = function () {
console.log(fn)
}
fn()
var obj = {
fn2: function () {
console.log(fn2)
}
}
obj.fn2()
输出一个函数fn
报错
(4)注意点
function test() { // var a = b = c =1; var a=1; b = 1; c =1; } test(); console.log(b,c) // 用var a= b = c =1 的方式声明变量会使b和c的值污染全局 .
使用变量之前,最好var 一个声明
function test() { // var a , b , c =1; var a; var b; var c=1; console.log(a,b,c) } test(); console.log(b,c) // 用var a , b , c =1;的方式声明变量,a , b的值为undefined
4.内存回收机制
JavaScript 具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。
这种垃圾收集机制的原理其实很简单:找出那些不再继续使用的变量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔周期性地执行这一操作。
变量的释放 -->瞬间的
内存的回收 -->周期性的
二、执行上下文和执行上下文栈
1.执行上下文(EC:execute context)
(1)什么是执行上下文
1.广义上来说: 执行上下文是一套非常严格的规则 2.狭义上来说: 执行上下文是浏览器的一个c++对象
执行上下文是动态的
执行上下文从属于其对应的作用域
多少个执行上下文= n+1 (n表示函数执行的次数)
一般情况下:一个时间点,一个作用域对应一个执行上下文
特殊情况下:一个时间点,一个作用域对应多个执行上下文 递归时(一个作用域很有可能对应多个执行上下文)
一个作用域中只会存在一个处于活动状态的执行上下文
(2)执行上下文分类
1. 全局执行上下文 2. 函数执行上下文
2.执行上下文栈
(1)压栈
1. 当全局代码开始执行前,先创建全局执行上下文环境
2. 当全局执行上下文环境创建好了以后将上下文中的所有内容放入栈内存
3. 最先放入的在最下边(global)
4. 其他执行的函数的执行上下文依次放入(放入的顺序是代码的执行顺序)
5. 栈中最后放入的执行完最先出栈。
(2)图解
(3)练习
1. 依次输出什么?
2. 整个过程中产生了几个执行上下文?
-->
<script">
//找变量 依据作用域链 找作用域所对应的活动状态的执行上下文中找对应的值
var i;
console.log(i)
i = 1
foo(1);
function foo(i) {
if (i == 4) {
return;
}
console.log(i);
foo(i + 1);
console.log(i);
}
console.log(i)
</script>
3.作用域和执行上下文栈
(1)区别1
1. 除全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时 2. 全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建 3. 函数执行上下文环境是在调用函数时, 函数体代码执行之前创建
(2)区别2
1. 作用域是静态的, 在编码时就被定义,只要函数定义好了就一直存在, 且不会再变化 2. 上下文环境是动态的, 调用函数时创建, 函数调用结束时上下文环境就会被释放
(3)联系
1. 上下文环境(对象)是从属于所在的作用域 2. 全局上下文环境==>全局作用域 3. 函数上下文环境==>对应的函数使用域
4.规则
1.全局执行上下文
在执行全局代码前将window确定为全局执行上下文 对全局数据进行预处理 1.var定义的全局变量==>undefined, 添加为window的属性 2.function声明的全局函数==>赋值(fun), 添加为window的方法 3. 提升 4.this==>赋值(window) 开始执行全局代码
console.log(window.parseInt("123PX"));
var a = "a-val";
function test(){
console.log("test");
}
console.log(window.a);
window.test();
console.log(this);
2.函数执行上下文
在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象 对局部数据进行预处理 1.形参变量==>赋值(实参) 2.arguments==>赋值(实参列表) 3.处理 var定义的局部变量 4.处理 function声明的函数 5.提升 6.this==>赋值(调用函数的对象)
开始执行函数体代码
function test() {
var a="a";
function inner() {
}
}
test();
window.test();
console.log(window.a);//原型链
console.log(a);//作用域链
3.变量的查找
如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的处于活动状态的执行上下文环境,再在其中寻找变量的值
5.提升
1.变量的提升
变量的提升 : 将变量的声明提到当前作用域的最顶层
var a;
console.log(a);
a =1;
2.函数的提升
函数的提升 : 将整个函数提到当前作用域的最顶层
function test() {
console.log("test");
}
test();
3.提升的注意点
函数的提升是整体的提升
变量的提升是声明的提升
函数的提升优于变量的提升
提升是指提升到本层作用域的最顶层
var a = 2;
var a = 3;
//
function test() {
console.log("test1")
}
function test() {
console.log("test2")
}
test();
4.练习
<script type="text/javascript"> /*测试题1: 先预处理变量, 后预处理函数*/ function a() {} var a; console.log(typeof a) // /*测试题2: 变量预处理, in操作符*/ var b; if (!(b in window)) { b = 1; } console.log(b) /*if (!(b in window)) { var b = 1; } console.log(b)*/ /*测试题3: 预处理, 顺序执行*/ function c(c) { console.log(c) var c = 3 } var c ; c = 1 c(2) /* var c = 1 function c(c) { console.log(c) var c = 3 } c(2)*/
/*测试题4*/
/* function test(){
function foo(){
console.log(1);
}
foo=5;
}
var foo;
foo=2;
test();
console.log(foo);*/
var foo=2;
test();
function test(){
foo=5;
function foo(){
console.log(1);
}
}
console.log(foo);
/*测试题5*/
/*function test(){
console.log(a);
}
var a;
test();
a=2;*/
test();
var a=2;
function test(){
console.log(a);
}
5.编码规范
1.使用变量前,一定要先声明(避免污染全局 迎合Js引擎的提升规则)
2.在分支结构中最好不要定义函数
6.this的基本规则
this指向谁,看当前函数的调用位置上的调用形式:
普通调用 this --> window 在严格模式下 this会指向undefined “use strict” -->严格模式 隐式调用 this --> 最近的调用者 显式调用 this --> 指定的对象 call() apply() bind(thisArg,arg1,arg2...) 硬绑定 bind()方法创建一个新的函数, 当这个新函数被调用时this键值为其提供的值, 其参数列表前几项值为创建时指定的参数序列。 构造调用 this --> 构造出来的实例对象
7.隐式丢失
<script> var obj={ name:"damu", test:function () { console.log(this); } } var fn = obj.test; fn(); </script>
隐式丢失:当以隐式调用(对象.)的形式,将一个函数给一个变量之后,这个变量居然独立的调用。
原本开发者的意图是想让test里面的this指向,指向Obj,可是最终变量居然独自调用了,this指向就变成window。这就叫隐式丢失
隐式丢失的最恐怖的场景:
看不到函数最终的执行位置
var obj={
name:"damu",
age:18,
test:function () {
console.log(this.age);
return 1;
}
}
setTimeout(obj.test.call(obj),2000)
//setTimeout(obj.test.bind(obj),2000) -->硬绑定 使用bind方法创建一个新的函数,这个新的函数跟原来的逻辑和实现方法是一样,
但是原函数的this指向是不确定的,新函数的this指向确定了。
解决了隐式丢失的问题,硬绑定确保了传进去的方法 this指向是定死的
8.js异步编程
<script> // 进程 线程 cpu // 进程是操作系统分配资源的最小单位 // 线程是进程中一个概念 (线程是程序执行的最小单元) //js引擎是单线程的 单线程(一个时间点只能干一件事情) //浏览器是多进程多线程的 //当js引擎遇到定时器时 会让浏览器通知异步线程来管理这个异步回调函数 js引擎继续解析下面的代码 //怎么管理异步回调函数? setTimeout(function () { console.log("setTimeout-回调") },2000) console.log("------") 先执行console.log("-----")。
::::::当js引擎遇到定时器时,会让浏览器通知异步线程来管理这个异步回调函数,放进异步队列,然后js引擎继续执行下面的代码
等后面的代码执行完毕之后,JS引擎再来执行异步队列里面的代码
<script>
// 这个2000毫秒 准不准? 不准 异步回调被执行的时间必定大于等于指定时间 需要等到定时器下面的代码全部执行完毕时再执行异步回调
setTimeout(function () {
console.log("setTimeout-回调")
},2000)
for(var i=0;i<10000;i++){
console.log("-------")
}
</script>
<script>
for(var i=0;i<5;i++){
setTimeout(function () {
console.log(i)
},1000)
}
</script>
输出:5个5
<script>
for(var i=0;i<5;i++){
setTimeout(function () {
console.log(i)
},1000*i)
}
// > 4000
for(var j=0;j<10000;j++){
console.log("-----")
}
</script>
这种情况下,当定时器下面的代码 执行时间大于4秒时,同时输出5个5.
当定时器下面没有代码执行时,隔一秒钟输出一个5.
三、闭包
1.理解闭包
(1)什么时候创建闭包
发生函数嵌套时,
外部函数的执行上下文被创建时(外部函数被调用时),就产生了闭包
(2)闭包放在了哪儿
内部函数的作用域中
(3)闭包有什么作用
1.使函数外部的变量在函数执行完之后,仍然活在内存中(延长了外部函数执行上下文中变量的生命周期)
2.让函数内部可以操作(读写)到函数外部的数据(变量/函数)
(4)创建外包的必要条件
1.函数嵌套
2.内部函数必须要使用到外部函数的变量(数据/函数)
3.调用外部函数
(5)闭包的生命周期
1. 产生: 在嵌套内部函数定义执行完时就产生了(不是在调用) 2. 死亡: 在嵌套的内部函数成为垃圾对象时
闭包一般不会主动销毁 当内部函数置为null时
(6)闭包的缺点
1. 缺点 * 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长 * 容易造成内存泄露(问题不大,占据的内存小 前端没有高并发!)
*内存溢出(有100M内存,使用了101M) 2. 解决 * 能不用闭包就不用 * 及时释放
(7)常见的闭包
1. 将函数作为另一个函数的返回值 2. 将函数作为实参传递给另一个函数调用
(8)重点
外部函数被调用时产生的闭包跟内部函数是一 一对应的关系
(9)闭包的应用
1. 具有特定功能的js文件 2. 将所有的数据和功能都封装在一个函数内部(私有的) 3. 只向外暴露一个包信n个方法的对象或函数 4. 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
(10)鸡肋闭包
function wrap() { var a="a-val"; function inner() { console.log(a); } inner(); } wrap();
当外部函数的执行上下文被调用时产生了一个闭包留下了一个a的值,
但是内部函数变量往上找能读取到a的值,这种情况下产生的闭包就是一个鸡肋闭包
(11)笔试题
面试题1:
<script type="text/javascript"> /* 说说它们的输出情况 */ //代码片段一 var name = "The Window"; var object = { name: "My Object", getNameFunc: function () { return function () { return this.name; }; } }; console.log(object.getNameFunc()()); //?
当console.log(object的属性getNameFunc()())时,最终读到了return this.name ,返回 name的值,
此时函数是一个普通调用(object.getNameFunc --> function() ),this指向的是window,
全局作用域定义了一个name的值为“The Window”,所以输出 The Window
//代码片段二 var name2 = "The Window"; var object2 = { name2: "My Object", getNameFunc: function () { var that = this;// this劫持 return function () { return that.name2; }; } }; console.log(object2.getNameFunc()()); //?
内部函数使用了外部函数的变量,产生了闭包that,that.name2等于this.name2
此时函数是一个对象.的形式的调用,this指向的是object2,object2 name属性的值是“My object”
所以输出My object
//异步 <script> //for(var i=0;i<5;i++){
// setTimeout(function () {
// console.log(i)
// },1000)
// } //闭包和内部函数是一一对应的关系 for(var i=0;i<5;i++){ (function (i) { setTimeout(function () { console.log(i) },1000*i) })(i) } </script> 外部函数被调用了5次,产生了5个闭包,i的值为别为0,1,2,3,4
函数内部产生了5个函数,虽然作用域一样,但函数不一样,产生的函数跟闭包一 一对应。
所以立即可执行函数一开始输出0,然后每隔一秒依次输出1,2,3,4。
面试题2: <script type="text/javascript"> /* 说说它们的输出情况 */ function fun(n, o) { console.log(o) return { fun: function (m) { return fun(m, n) } } } var a = fun(0) a.fun(1) // -- var b = fun(1,0) a.fun(2) // var c = fun(2,0); a.fun(3) // var d = fun(3,0) // undefined,0,0,0 var b = fun(0).fun(1).fun(2).fun(3) //undefined,0,1,2 /* var 对象1号 = fun(0); var 对象2号 = 对象1号.fun(1); var 对象3号 = 对象2号.fun(2); var 对象4号 = 对象3号.fun(3); */ var c = fun(0).fun(1) c.fun(2) c.fun(3) //undefined,0,1,1 /* var 对象1号 = fun(0); var 对象2号 = 对象1号.fun(1); 对象2号.fun(2); 对象2号.fun(3); */ </script>
四.原型与原型链
1.理解原型对象
(1)函数的prototype属性
1.每个函数都有一个prototype属性,它默认指向一个Object空对象(即称为原型对象)
2.原型对象中有一个属性constructor,它指向函数对象
(2)操作原型对象
1.可以通过prototype属性找到原型对象,并可以给该对象添加属性和方法(通常是添加方法)
2.作用:函数所有的实例对象自动拥有原型中的属性(方法)
2.原型的分类
(1)显式原型与隐式原型
1.每个函数function都有一个prototype,即显式原型
2.每个实例对象都有一个__proto__,可称为隐式原型
3.对象的隐式原型的值为其对应构造函数的显式原型的值。
(2)图解
注意点:
1.所有的原型对象都有一个constructor属性 指向原函数
2.所有的原型对象都是一个{}
所有的原型对象.__proto__ === Object.prototype
3.Object.prototype.__proto__ === null
(3)总结
1.函数的prototype属性:在定义函数时自动添加的,默认值是一个空Object对象 2.对象的__proto__属性:创建对象时自动添加的,默认值为构造函数的prototype属性值
3.程序员能直接操作显式原型,但不能直接操作隐式原型(ES6之前)
3.原型链
(1)概念
原型服务于 属性的查找和设置
1.访问一个对象的属性时,先在自身属性中查找,如果找到 直接返回
2.如果没有,再沿着__proto__这条链向上查找,找到返回
3.如果最终没找到,返回undefined
4.__proto__这条原型查找链就是原型链:隐式原型链
(2)作用
<script> // 原型服务于 属性的查找与设置 /*属性查找: 先在对象的直接属性中找 如果找到 直接返回 如果找不到上原型链!!(隐式原型链) obj.__proto__ Object.prototype 如果找到 直接返回 如果找不到 返回undefined */ var obj={ toString:123 } console.log(obj.toString) console.log(obj.valueOf) </script>
(3)原型链属性问题
1.读取对象的属性值时:会自动到原型链中查找
2.设置对象的属性值时:不会查找原型链,如果当前对象中没有此属性,直接添加此属性并设置其值
3.方法一般定义在原型中,属性一般通过构造函数定义在对象本身上
(4)方法的重写
var arr=[1,2,3];
console.log(arr.toSring()) //1,2,3 数组自己重写的toString 方法
console.log(Object.prototype.toString.call(arr)) //[object Array] Object.prototype上的toString方法
arr.__proto__ -->Array.prototype
Array.prototype.__proto__ --->Object.prototype
Object.prototype.__proto__ --->null
Array.prototype里面的toString方法 把Object.prototype里面的toString方法覆盖了
这就叫做方法的重写
(5)练习题
<script type="text/javascript"> // 1. 函数内部的语句跟函数的属性没有关系 // 2.函数的提升
// 3.变量的提升
// 4.this的指向 Foo.getName = function () { alert (2); }; function getName() { alert (5); } Foo.prototype.getName = function () { alert (3); }; function Foo() { getName = function () { alert (1); }; return this; } getName = function () { alert (4); }; Foo.getName(); // 2 getName(); //4 Foo().getName(); //1 getName(); //1 new Foo.getName(); // 2 new Foo().getName()//3 foo.getName() </script>
<script type="text/javascript"> var A=function(){} A.prototype.n=1 var b=new A() // 改了构造函数的显式原型后 之前创建的实例对象的隐式原型会不会被矫正过来? 不会改!!! A.prototype={ n:2, m:3 } //A.prototype.n=2; //A.prototype.m=3; var c=new A() console.log(A.prototype) // {n:2,m:3} console.log(b.n,b.m,c.n,c.m)// 1 undefined 2 3 </script>
上面题目的内存结构图:
4.instanceof
(1)概念
1.表达式:A instanceof B
2.如果B函数的显式原型对象在A对象的原型链上,返回true,否则返回false
(2)练习
//怎么判断一个对象是不是数组 // 1.isArray // 2.使用Object.prototype.toString function isArray(obj) { //数组 : "[object Array]" //String : "[object String]" return Object.prototype.toString.apply(obj).indexOf("Array") === 8; } console.log(isArray([])); //3. a(对象) instanceof b(函数) // b.prototype 是否出现在 a的隐式原型链上 console.log([] instanceof Array)
function Person() {
}
var p = new Person();
p.__proto__ = Array.prototype; //修改了原型链
console.log(p instanceof Person)
//使用instanceof规则时,得先看对象的原型链是什么样子