JavaScript高级日记(二)函数高级

目录

原型

执行上下文

作用域

闭包


原型

每个函数都有一个prototype属性,它默认指向一个Object空对象(原型对象)。原型对象中有一个属性constructor,它指向函数对象。给原型对象添加属性(一般都是方法)可以使函数的所有实例对象自动拥有原型中的属性(方法)。

console.log(typeof Date.prototype);//Object
console.log(Date.prototype.constructor===Date);//true
function fun(){
    console.log('fun');
}
fun.prototype.test=function(){
    console.log('test');
}
var fun=new fun();//fun
fun.test();//test

显式原型与隐式原型:

每个函数都有一个prototype,即显式原型(属性),在定义函数时自动添加,默认值是一个空Object对象;每个实例对象都有一个__proto__,可称为显式原型(属性),创建对象时自动添加,默认值为构造函数的prototype属性值。实例对象的隐式原型的值为其对应的构造函数的显式原型的值。

原型链(隐式原型链):

访问一个对象的属性时,先在自身属性中查找,找到返回;如果没有,再沿着__proto__这条链向上查找,找到返回;如果最终没找到,返回undefined。原型链的作用就是查找对象的属性(方法)。所有函数的__proto__都是一样的,都是由new function产生的。

var o1=new Object();
var o2={ }

function Foo(){ }
var Foo=new Function();
Function=new Function();

(弹幕:函数由Function产生,Function唯一不同的就是它的__proto__指向它自己;函数的__proto__(隐式原型)指向它父亲,也就是Function的prototype(显式原型),也就是说Function __proto__ 找不到它的父亲了,但是Function的prototype在Function创建后就存在了。)

原型继承:构造函数的实例对象自动拥有构造函数原型对象的属性(方法),利用的就是原型链。

1.函数的显式原型指向的对象默认是空Object实例对象,但Object不满足。

2.所有函数都是Function的实例,包含Function。

3.Object的原型对象是原型链的尽头。

原型链属性问题:

1.读取对象的属性值时,会自动到原型链中查找

2.设置对象的属性值时,不会查找原型链,如果当前对象中没有此属性,直接添加此属性并设置其值

3.方法一般定义在原型中,属性一般通过构造函数定义在对象本身上

instanceof:A  instanceof  B

如果B函数的显式原型对象在A的原型链上,返回true,否则返回false

Function是通过new自己产生的实例

function Foo(){}
var f1=new Foo();
console.log(f1 instanceof Foo);//true
console.log(f1 instanceof Object);//true
console.log(Object instanceof Function);//true
console.log(Object instanceof Object);//true
console.log(Function instanceof Function);//true
console.log(Function instanceof Object);//true
console.log(Object instanceof Foo);//false

 练习:

//--1--
var A=function(){}
A.prototype.n=1;
var b=new A();
A.prototype={
    n:2,
    m:3
};
var c=new A();
console.log(b.n,b.m,c.n,c.m);//1 undefined 2 3
//--2--
var F=function(){}
Object.prototype.a=function(){
    console.log('a()');
}
Function.prototype.b=function(){
    console.log('b()');
}
var f=new F();
f.a();//a()
f.b();//f.b is not a function
F.a();//a()
F.b();//b()

执行上下文

声明提升:

变量声明提升:通过var定义的变量,在定义语句之前就可以访问到,值为undefined

函数声明提升:通过function声明的函数,在之前就可以直接调用,值为函数定义(对象)

var a=3;
function fn(){
    console.log(a);
    var a=4;
}
fn();//undefined 
//函数先从自身寻找a,找到了已定义未赋值的a,值为undefined,不会再向上级作用域寻找
fn2();//fn2
function fn2(){
    console.log('fn2');
}
fn3();//报错
var fn3=function(){
    console.log('fn3()');
}

执行上下文:

代码位置分为全局代码和函数(局部)代码。

全局执行上下文:

在执行全局代码前将window确定为全局执行上下文

对全局数据进行预处理:var定义的全局变量->undefined,添加为window的属性;function声明的全局函数->赋值(fun),添加为window的方法;this->赋值window

开始执行全局代码

函数执行上下文:

在调用函数、准备执行函数体之前,创建对应的函数执行上下文对象(虚拟的,存在于栈中)

对局部数据进行预处理:形参变量->赋值(实参)->添加为执行上下文的属性;arguments->赋值(实参列表),添加为执行上下文的属性;var定义的局部变量->undefined,添加为执行上下文的属性;function声明的函数->赋值(fun),添加为执行上下文的方法,this->赋值(调用函数的对象)

开始执行函数体代码

// 全局执行上下文
console.log(a1,window.a1);//undefined undefined
a2();//a2
console.log(this);//window
var a1=3;
function a2(){
    console.log('a2');
}
console.log(a1);//3
console.log('----------');
// 函数执行上下文
function fn(a1){
    console.log(a1);//2
    console.log(a2);//undefined
    a3();//a3
    console.log(this);//window
    console.log(arguments);//伪数组[2,3]
    var a2=3;
    function a3(){
        console.log('a3');
    }
}
fn(2,3);

执行上下文栈:

1.在全局代码执行前,JS引擎就会创建一个栈来存储管理所有的执行上下文对象

2.在全局执行上下文(window)确定后,将其添加到栈中(压栈)

3.在函数执行上下文创建后,将其添加到栈中(压栈)

4.在当前函数执行完后,将栈顶的对象移除(出栈)

5.当所有的代码执行完后,栈中只剩下window

//进入全局执行上下文
var a=10;
var bar=function(x){
    var b=5;
    foo(x+b);//进入foo函数执行上下文
}
var foo=function(y){
    var c=5;
    console.log(a+c+y);;
}
bar(10);//30 进入bar函数执行上下文
bar(6);//26 进入bar函数执行上下文
// 共产生五次执行上下文,window产生一次,bar产生两次,foo被调用两次所以产生两次

上图是调用一次bar的情况,执行上下文栈的栈底一定是window

练习:

console.log('global begin:'+i);
var i=1;
foo(1);
function foo(i){
    if(i==4){
        return;//如果i=4就退出当前执行的函数,也就是退出foo(4)
    }
    console.log('foo begin:'+i);;
    foo(i+1);//递归,相当于嵌套调用
    console.log('foo end:'+i);
}
console.log('global end:'+i);
/**整个过程共产生五个执行上下文
 * 输出:
 * global begin:undefined
 * foo begin:1
 * foo begin:2
 * foo begin:3
 * foo end:3
 * foo end:2
 * foo end:1
 * global end:1*/
// ------------------------------
function a(){}
var a;
console.log(typeof a);//function
//先执行变量提升,再执行函数提升,函数提升优先级高于变量提升,且不会被同名变量声明时覆盖,但是会被变量赋值后覆盖
// ------------------------------
if(!(b in window)){
    var b=1;//var提升变量,b在if语句内赋值
}
console.log(b);//undefined
// ------------------------------
var c=1;
function c(c){
    console.log(c);
}
c(2);//报错c is not a function,变量被赋值后覆盖函数提升

作用域

作用域与全局上下文:

区别1:

全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了,而不是在函数调用时;全局执行上下文环境是在全局作用域确定之后,js代码马上执行之前创建;函数执行上下文是在调用函数时,函数体代码执行之前创建

区别2:

作用域是静态的,只要函数定义好了就一直存在,且不会再变化;上下文环境是动态的,调用函数时创建,函数调用结束时上下文环境就会被释放

联系:

执行上下文环境(对象)从属于所在的作用域;全局上下文环境==>全局作用域;函数上下文环境==>对应的函数作用域

作用域链:

多个上下级关系的作用域形成的链,它的方向是从下向上(从内到外)的,查找变量时就是沿着作用域链来查找的

查找一个变量的查找规则:在当前作用域下的执行上下文中查找对应的属性,如果有直接返回,如果没有则在上一级作用域的执行上下文中查找对应的属性,如果有直接返回,否则再向上一级查找直至找到全局作用域,如果还找不到则抛出异常

var a=10,b=20;
function fn(x){
    var a=100,c=300;
    console.log('fn()',a,b,c,x);
    function bar(x){
        var a=1000,d=400;
        console.log('bar()',a,b,c,d,x);
    }
    bar(100);
    bar(200);
}
fn(10);
// fn() 100 20 300 10
// bar() 1000 20 300 400 100
// bar() 1000 20 300 400 200

练习:

var x=10;
function fn(){
    console.log(x);
}
function show(f){
    var x=20;
    f();
}
show(fn);//10
// --------------------
var fn2=function(){
    console.log(fn2);
}
fn2();//ƒ (){console.log(fn2);}
var obj={
    f:function(){
        // obj是一个变量,f是obj的属性;只有函数会分割作用域,所以只有此{}和全局两个作用域
        console.log(f);
        // 这里指打印window的f,如果想输出f则需要 console.log(this.f);
    }
}
obj.f();//报错 f is not defined

闭包

当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量时,就产生了闭包,闭包存在于嵌套的内部函数中。使用Chrome调试查看,大部分人理解为闭包是嵌套的内部函数,少数人理解为闭包是包含被引用变量(函数)的对象(弹幕:闭包是能够读取其他函数内部变量的函数,本质上,闭包是将函数的内部和外部连接起来)。产生闭包的条件:函数嵌套、内部函数引用了外部函数的数据(变量/函数)

闭包的作用:

1.使用函数内部的变量在函数执行完后仍然存活在内存中(延长了局部变量的生命周期)

2.让函数外部可以操作(读写)到函数内部的数据(变量/函数)。按照面向对象的概念,闭包就是暴露一个方法,以供外部可以访问内部的变量

闭包的生命周期:

产生:在嵌套内部函数定义执行完时就产生了(不是在调用时)

死亡:在嵌套的内部函数成为垃圾对象时

// 一、将函数作为另一个函数的返回值
function fn1(){
    // 产生闭包(函数提升,内部对象已经创建)
    var a=2;
    function fn2(){
        a++;
        console.log(a);
    }
    return fn2;
    // 只有返回值是fn2(而非fn2())且var f=fn1()这样调用,两次输出结果才会不一样
}
// 函数fn1被调用一次,产生一个闭包,函数fn2被调用两次
// 闭包没消失是因为被保存在全局变量中,不会被销毁;fn1没销毁是因为fn2需要用到fn1的属性、方法
// 当f=fn1时,由于是赋值操作将地址赋值给f,所以f和fn1都指向一个内存空间,所以fn1执行完不能释放因为f还指向这个内存空间
/**弹幕
 * 1.fn2作为返回值传递给f(),所以fn2包含的闭包也在f()中
 * 2.f()是全局变量,自然,在f()里的闭包也在全局作用域里
 * 3.所以后续的调用都是在全局作用域中执行,不是在之前的局部作用域中
 * 注意执行上下文栈和这个闭包的区别
 * 执行上下文是将当前执行的上下文进行压栈,执行完后就出栈
 * fn1执行完就出栈,但是返回的值fn2包含fn1的闭包*/ 
var f=fn1();
f();//3
f();//4
// 闭包死亡(包含闭包的函数对象成为垃圾对象)
// 如果像下面这样写则两次都会输出3
// 因为返回值是fn2而不是fn2(),所以不能写成fn1()调用
// fn1()();
// fn1()();
// 问题:
// 1.函数执行完后,函数内部声明的局部变量是否还存在?
// 一般不存在,存在于闭包中的变量才可能存在
// 2.在函数外部能直接访问函数内部的局部变量吗?
// 不能,但我们可以通过闭包让外部操作它
// 二、将函数作为实参传递给另一个函数调用
function showDelay(msg,time){
    setTimeout(function(){
        alert(msg);
    }, time);
}
// 闭包里有msg没有time
showDelay('attml',2000);

闭包的应用:

定义JS模块(具有特定功能的js文件):将所有的数据和功能都封装在一个函数内部(私有的),只向外暴露一个包含n个方法的对象或函数,模块的使用者只需要通过模块暴露的对象调用方法来实现对应的功能

test.html

<script src="test2.js"></script>
<script src="test3.js"></script>
<script>var module=myModule();
    module.doSomething();//doSomething:AT TML
    module.doOtherthing();//doOtherthing:at tml
    // ------------------------------
    myModule2.doSomething();//doSomething:AT TML
    myModule2.doOtherthing();//doOtherthing:at tml
</script>

test2.js

function myModule(){
    var msg='At Tml';
    function doSomething(){
        console.log('doSomething:'+msg.toUpperCase());
    }
    function doOtherthing(){
        console.log('doOtherthing:'+msg.toLowerCase());
    }
    return {//向外暴露对象
        doSomething:doSomething,
        doOtherthing:doOtherthing
    };
}

test3.js

// 匿名函数自调用
// 在括号内传递window参数可以在代码压缩时将window压缩为w
(function(/*window*/){
    var msg='At Tml';
    function doSomething(){
        console.log('doSomething:'+msg.toUpperCase());
    }
    function doOtherthing(){
        console.log('doOtherthing:'+msg.toLowerCase());
    }
    window.myModule2={
        doSomething:doSomething,
        doOtherthing:doOtherthing
    }
})(/*window*/)

闭包的缺点及解决方法:

缺点:

1.内存溢出,函数执行完后,函数内的局部变量没有释放,占用内存时间会变长

2.内存泄漏

解决:尽量不用闭包;及时释放(f=null;)

/**内存溢出
 * 一种程序运行出现的错误,当程序运行需要的内存超过了剩余内存时,就抛出内存溢出的错误
 * 内存泄漏
 * 占用的内存没有及时释放,内存泄漏积累多了就容易导致内存溢出
 * 常见的内存泄漏:*/
// 意外的全局变量
function fn(){
    a=3;//没有用var(或let等)直接声明会定义为window的属性
    console.log(a);
}
fn();//3
console.log(a+3);//6
// 没有及时清理的计时器或回调函数
var intervalID=setInterval(function(){//启用循环定时器后不清理
    console.log('interval');
}, 1000);
// 闭包
function fn1(){
    var a=7;
    function fn2(){
        console.log(++a);
    }
    return fn2;
}
var f=fn1()
f();//8
// f=null;

练习:

// --- 1 ---(没有闭包)
var name='The Window';
var object={
    name:'My Object',
    getNameFunc:function(){
        return function(){
            return this.name;
        };
    }
};
alert(object.getNameFunc()()); //The Window
// 直接执行(匿名)函数里的this指向window
// -- 2 --- (有一个闭包)
var name2='The Window';
var object2={
    name2:'My Object',
    getNameFunc:function(){
        var that=this;//that就是object2
        return function(){
            return that.name2;
        };
    }
};
// 通过保存object2的this,在闭包中获取this指向obj
alert(object2.getNameFunc()()); //My Object
 // --- 3 ---
 function fun1(n,o){
     console.log(o);
     return{
         fun:function(m){
             return fun1(m,n);
             // n的值传给o
         }
     };
 }
 // 每次调用函数都会使用上层的闭包
 var a=fun1(0); a.fun(1); a.fun(2); a.fun(3);//undefined 0 0 0 
 // a.fun(1)相当于m=1,此时这个对象里的方法返回了fun(1,0)的执行结果
 var b=fun1(0).fun(1).fun(2).fun(3);//undefined 0 1 2
 // 拿到返回的函数后直接调用
 // 链式执行会导致n的改变,n一直是前面的函数执行的形参
 // fun(0):n=0,o=undefined;.fun(1):n=m=1,o=n=0;.fun(2):n=m=2,o=n=1;.fun(3):n=m=3,o=n=2
 var c=fun1(0).fun(1); c.fun(2); c.fun(3);//undefined 0 1 1
 // fun(0).fun(1)同上,n=m=1,o=n=0
 // .fun(2)同上,n=m=2,o=n=1
 // fun(0).fun(1).fun(3):n=m=3,o=n=1

(视频:B站尚硅谷)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值