bind函数_第27节 函数、作用域及垃圾回收-Javascript-零点程序员-王唯

本内容是《Web前端开发之Javascript视频》的课件,请配合大师哥《Javascript》视频课程学习。

私有变量:

严格来讲,Javascript中没有私有成员的概念;所有对象属性都是公有的,但是有一个私有变量的概念;任何在函数中定义的变量,都可能被认为是私有变量,因为不能在函数的外部访问这些变量;

私有变量包括函数的参数、局部变量和在函数内部定义的其他函数;如:

function add(num1,num2){    var sum = num1+num2;    return sum;}

可以把有权访问私有变量和私有函数的公有的方法称为特权方法(privileged method),利用私有和特权成员,可以隐藏那些不应该被直接修改的数据;有两种在对象上创建特权方法的方式;第一种是在构造函数中定义特权方法,如:

function Person(name,age){    // 此处不使用this的原因,是想隐藏内部数据    // this.myName = name;      // this.myAge = age;    var myName = name;    var myAge = age;    this.getName = function(){        return myName;    };    this.setName = function(value){        myName = value;    };    this.getAge = function(){        return myAge;    };    this.setAge = function(value){        myAge = value;    }}var person = new Person("wangwei",18);console.log(person.getName());person.setName("Wujing");console.log(person.getName());person.setAge(person.getAge()+1);console.log(person.getAge());

这种方式,因为每次调用构造函数都会重新创建其中的所有方法,这显然不是必须的,也是一个缺点,使用静态私有变量来实现特权方法就可以避免这个问题;

静态私有变量:

通过在私有作用域中定义私有变量或函数,同样可以创建特权方法;

这个模式与在构造函数中定义特权方法的主要区别,就是在于构造函数中的私有变量和函数是由实例共享的;而特权方法是在原型上定义的,因此所有实例都使用同一个函数;而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用,如:

(function(){    var site,domain;    MyObject = function(s,d){        site = s;        domain = d;    };    MyObject.prototype.getSite = function(){        return site;    };    MyObject.prototype.setSite = function(value){        site = value;    };    // 再添加getDomain及setDomain方法})();var website = new MyObject("零点网络","www.zeronetwork.cn");console.log(website.getSite());website.setSite("zeronetwork");console.log(website.getSite());var p = new MyObject("王唯个人网站","www.lingdian.com");console.log(website.getSite());console.log(p.getSite());

以这种方式创建静态私有变量会让每个实例都没有自己的私有变量;到底是使用实例变量,还是静态私有变量,最终还是看具体的需求;

函数的属性和方法:

因为函数是对象,所以函数也有属性和方法;如length属性;

name属性,非标准,通过这个属性可以访问到函数的名字;

function show(a,b,c){console.log(arguments.length);}console.log(show.name);  // show

如果是使用new Function()定义的,会返回anonymous;如:

var show = new Function();console.log(show.name);  // anonymous

使用函数表达式也可以返回函数名字;

var show = function(){console.log("func")};console.log(show.name);  // show

caller属性:

该属性保存着调用当前函数的函数的引用;如果是在全局作用域中调用当前函数,它的值为null;

function outer(){inner();}function inner(){console.log(inner.caller);}outer();

为了实现更松散的耦合,也可以通过arguments.callee.caller来访问相同的信息,如:

function inner(){console.log(arguments.callee.caller);}

注:当在严格模式下运行时,arguments.callee会导致错误;

注:在严格模式下,还有一个限制:不能为函数的caller属性赋值,否则导致错误;

prototype属性:

在ES核心所定义的全部属性中,最有意思的就是prototype属性了,其表示函数的原型;对于ES中的引用类型来说,prototype是保存它们所有实例方法的真正所在;换句话说,诸如toString()和valueOf()等方法实际上都保存在prototype属性中,只不过是通过各自对象的实例访问罢了;在创建自定义引用类型以及实现继承时,prototype属性的作用是极为重要的;在ES中,prototype属性是不可枚举的,因此使用for-in无法发现;

apply()和call():

每个函数都包含这两个方法;这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值;

apply()方法接收两个参数:一个是在其中运行该函数的作用域,另一个是参数数组;其中第二个参数可以是Array的实例,也可以是arguments对象;如:

function sum(num1,num2){return num1+num2;}function callSum1(num1,num2){    return sum.apply(this,arguments);  //传入arguments对象}function callSum2(num1,num2){    return sum.apply(this,[num1,num2]);  //传入数组}console.log(callSum1(10,20));console.log(callSum2(10,20));

注:在严格模式下,未指定环境对象而调用函数,则this值不会指向window;

call()方法也接受两个以上参数,第一个参数是与apply()的第一个参数相同,但其余参数都直接传递给函数;换句话说,在使用call()时,传递给函数的参数必须逐个列举出来,如:

function sum(num1,num2){return num1+num2;}function callSum(num1,num2){    return sum.call(this,num1,num2);}alert(callSum(10,20));

其真正的apply()和call()作用是能够扩充函数赖以运行的作用域;如:

window.color="red";var o={color:"blue"};function sayColor(){console.log(this.color);}sayColor();  // redsayColor.call(this);  // redsayColor.call(window);  // redsayColor.call(o);  // blue

使用call()或apply()来扩充作用域的最大好处,就是对象不需要与方法有任何耦合关系;

bind()方法:主要作用就是将函数绑定至某个对象;

语法:fun.bind(this,arg1,arg2,...);

该方法会创建一个新的函数,称为绑定函数,其可传入两个参数,第一个参数作为this,第二个及以后的参数则作为函数的参数调用;即调用新的函数会把原始的函数当作对象的方法来调用;如:

window.color="red";var o={color:"blue"};function sayColor(){console.log(this.color);}var objectSayColor = sayColor.bind(o);objectSayColor();  // blue
var x = 10;function fun(y){return this.x + y;}var o = {x:1};var g = fun.bind(o);console.log(fun(5));  // 15console.log(g(5));  // 6

有些浏览器可能不支持bind方法,兼容性的做法:

function bind(f,o){    if(f.bind) return f.bind(o);    else return function(){        return f.apply(o,arguments);    }}

为bind()方法传入参数,该参数也会绑定至this;这种应用是一种常见的函数式编程技术,也被称为“柯里化”(currying),如:

var sum = function(x,y){return x + y;};// 创建一个类似sum的新函数,但this的值绑定到null// 并且第一个参数绑定到1,这个新的函数期望只传入一个实参var succ = sum.bind(null,1);console.log(succ(2));  // 3 x绑定到1,并传入2作为实参y// 又如function f(y,z){return this.x + y + z};  // 累加计算var g = f.bind({x:1},2);    // 绑定this和yconsole.log(g(3));      // 6,this.x绑定到1,y绑定到2,z绑定到3

bind()方法返回的新函数,该函数对象的length属性是绑定函数的形参个数减去绑定实参的个数,即调用新函数时所期望的实参的个数,如:

var sum = function(x,y,z){return x + y + z;};var o = {};var fun = sum.bind(o,1);   // 3 - 1 = 2console.log(fun(2,3));  // 6console.log(fun.length);  // 2

使用bind()方法也可以用做构造函数,当bind()返回的函数用做构造函数时,将忽略传入的bind()的this;如:

var sum = function(x,y,z){    this.x = x;    this.y = y;    this.z = z;    this.getNum = function(){        return this.x + this.y + this.z + this.a;    }};var o = {a:1};var fun = sum.bind(o,1);var myFun = new fun(8,9,10);  console.log(myFun);console.log(myFun.getNum());  // NAN

高阶函数:

所谓高阶函数(higher-order function)就是操作函数的函数,它接收一个或多个函数作为参数,或者返回一个函数;如:

var powFun = function(x){    return Math.pow(x,2);};function add(f,x,y){    return f(x) + f(y);}console.log(add(powFun,3,4));  // 25

其实数组中有关迭代的方法全是高阶函数;比如,典型的一个应用,数组对象的map()方法,如:

function pow(x){    return Math.pow(x,2);}var arr = [1,2,3,4,5];var result = arr.map(pow);console.log(result); // 所返回的函数的参数应当是一个实参数组,并对每个数组元素执行函数f()// 并返回所有计算结果组成的数组function mapper(f){    return function(a) {return a.map(f);};}var increment = function(x){return x + 1;}var incrementer = mapper(increment);console.log(incrementer([1,2,3]));

更常见的应用:

function not(f){    return function(){  // 返回新的函数        var result = f.apply(this,arguments);   // 调用f()        return !result;     // 结果求反    };}var even = function(x){  // 判断是否为偶数    return x % 2 === 0;};var odd = not(even);console.log([1,1,3,5,5].every(odd));  // true 每个元素都是奇数 // 返回一个新的可以计算f(g(...))的函数// 返回的函数h()将它所有的实参传入g(),然后将g()的返回值传入f()// 调用f()和g()时的this值和调用h()时的this值是同一个thisfunction compose(f,g){    return function(){        // 需要给f()传入一个参数,所以使用f()的call()方法        // 需要给g()传入很多参数,所以使用g()的apply()方法        return f.call(this, g.apply(this, arguments));    };}var square = function(x){return x*x;};var sum = function(x,y){return x + y;};var squareofsum = compose(square, sum);console.log(squareofsum(2,3));  // 25

递归:

递归是指函数调用自己;

语法:

function f1(){

f1();

}

隐含递归:

function f1(){…; f2(); …}

function f2(){…; f1(); …}

通过递归打印出1-9的数值,如:

function printNum(n){    if(n>=1){        printNum(n - 1);    }    console.log(n);}printNum(9);

递归函数效率低,但有利于理解和解决现实问题;

递归函数的执行过程:第一阶段”回推”,第二阶段”递推”;

函数在适当的时候能结束递归,否则会进入死循环;

function test(n){    console.log("a" + n);    n++;    if(n<=5){        test(n);    }    console.log("b" + n);}test(1);  // 12345665432

又如:

// 5个人,第5个人比第4个人大2岁,...第一个人10岁,第5个人几岁?function age(n){    if(n == 1){        return 10;    }else{        return age(n - 1) + 2;    }}console.log("第5个人的年龄为:" + age(5));

阶乘:

function factorial(num){    if(num<=1){        return 1;    }else{        return num*factorial(num-1);    }}

注:但是如果类似于以下的代码,就会出错:

var anotherFactorial = factorial;factorial = null;alert(anotherFactorial(4));

在这种情况下,如果函数内部可以使用arguments.callee就可以解决问题;其指向正在执行的函数的指针,因此可以用它来实现对函数的递归调用:

return num*arguments.callee(num-1);

但在严格模式下,不能通过访问arguments.callee,访问这个属性会导致错误;不过,可以通过使用命名函数表达式来达到相同的效果;如:

var factorial = (function f(num){    if(num<=1){        return 1;    }else{        return num*f(num-1);    }});var anotherFactorial = factorial;factorial = null;alert(anotherFactorial(4));

垃圾回收:

JS实现了垃圾自动回收处理机制,即,执行环境会负责管理代码执行过程中使用的内存,会自动分配、释放内存;在其他语言中,一般是手工跟踪内存的使用情况,比如C语言,开发人员可以显式的分配和释放系统的内存;但在JavaScript中,开发人员不用关心内存使用问题,所需内存的分配以及无用内存的回收完全实现了自动管理;其实现的原理是:找出那些不再继续使用的变量,然后释放其占用的内存;为此垃圾回收器会按照固定的时间间隔或在某个预定的收集时间,周期性地执行;

var a = "zero";var b = "network";a = b; // "zero" 所占空间被释放

变量的生命周期:

无论哪种开发语言,其内存的生命周期几乎是一样的:分配内存空间-使用内存空间-释放空间;

函数中局部变量的正常生命周期:只在函数执行的过程中存在;而在这个过程中,会为局部变量在栈或堆内存上分配相应的空间,以便存储它们的值;当函数执行结束,局部变量就没有存在的必要了,因此可以释放它们的内存以供将来使用;在这种情况下,很容易判断变量是否还有存在的必要;但并非所有情况下都这么容易判断;垃圾收集器必须跟踪哪个变量有用哪个变量没有用,对于不再有用的变量打上标记,以备将来收回其占用的内存;用于标识无用变量的策略可能会因实现而异,但具体到浏览器中的实现,则通常有两个策略;

1)标记清除

JavaScript中最常用的垃圾收集方式是标记清除(mark-and-sweep);当变量进入环境时,就将这个变量标记为“进入环境”;

垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。

目前,各浏览器使用的都是标记清除的策略,只不过垃圾收集的时间间隔互相不同。

2)引用计数

另一种不太常见的垃圾收集策略叫做引用计数(reference counting);引用计数的含义是跟踪记录每个值被用的次数;当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1,如果同一个值又被赋给另一个变量,则该值的引用次数加1;相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1;当这个值的引用次数变成0时,则说明没有办法再访问这个值了;因而就可以将其占用的内存空间回收回来;这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的空间了。

Navigator3是最早使用引用计数策略的浏览器,但遇到了一个严重的问题:循环引用;即对象A中包含指向对象B的指针,而对象B中也包含一个指向对象A的引用,如:

function problem(){    var objectA = new Object();    var objectB = new Object();    objectA.other = objectB;    objectB.another = objectA;}

为此,Navigator4中放弃了引用计数方式,转而采用标记清除来实现其垃圾收集机制;

但是,IE中某些对象还在采用引用计数方式,这些对象不是原生的Javascript对象,如BOM和DOM中的对象就是使用C++以COM对象的形式实现的,而COM对象的垃圾收集机制采用的就是计数策略;因此,即使IE的JavaScript引擎是使用标记清除策略来实现的,但Javascript访问的COM对象依然是基于引用计数策略的;换句话说,只要在IE中涉及COM对象,就会存在循环引用的问题;如:

var element = document.getElementById("some_element");var myObject = new Object();myObject.element = element;element.someObject = myObject;

由于存在这个循环引用,即使将示例中的DOM从页面中移除,其也永远不会被回收;

为了避免类似这样的循环引用问题,最好是在不使用它们的时候手工断开原生JavaScript对象与DOM元素之间的连接,如:myObject.element = null;element.someObject = null;

目前,IE早已把BOM和DOM对象都转换成了真正的JavaScript对象;这样,就避免了两种垃圾收集算法并存导致的问题,也消除了常见的内存泄漏现象;

3)管理内存:

使用具备垃圾收集机制的语言编写程序,开发人员一般不必要操心内存管理的问题;但是,JavaScript在进行内存管理及垃圾收集时面临的问题还是与众不同;其中最主要的一个问题,就是分配给Web浏览器的可用内存数量通常要比分配给桌面应用程序的要少;这样做的目的主要是出于安全方面的考虑,目的是防止运行JavaScript的网页耗尽全部系统内存而导致系统崩溃;内存限制问题不仅会影响给变量分配内存,同时还会影响调用栈以及在一个线程中能够同时执行的语句数量。

因此,确保占用最少的内存可以让页面获得更好的性能;而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据;一旦数据不再有用,最好通过将其值设置为null来释放其引用,即解除引用(dereferencing),其适用于大多数全局变量和全局对象的属性;如:

function createPerson(name){    var localPerson = new Object();    localPerson.name = name;    return localPerson;}var globalPerson = createPerson("wangwei");globalPerson=null;  // 手工解除globalPerson的引用

注:解除一个值的引用并不意味着自动回收该值所占用的内存;解除引用的值作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。

JS的自动内存管理存在一些问题,例如垃圾回收实现可能存在缺陷或者不足,因此,需要找到一个合适的解决方法;

内存泄露:

Javascript的几种内存泄露:

1、全局变量:

一个没有声明的变量,成为了一个全局变量;因此,要避免这种情况出现,或者使用严格模式;

2、循环引用:

即:A引用B,B引用A,如此,其引用计数都不为0,所以不会被回收;

解决:手工将它们设为null;

3、闭包:

闭包会造成对象引用的生命周期脱离当前函数的作用域,使用不当,会造成内存泄露;

4、延时器、定时器:

setInterval / setTimeout中的this指向的是window对象,所以内部定义的变量也挂载在了全局,if引用了someResource变,如果没有清除setInterval/setTimeout的话,someResource得不到释放;

var someResource = getData();setInterval(function(){    var node = document.getElementById('Node');    if(node){        node.innerHTML = JSON.stringify(someResource);    }},1000);

5、DOM引用的内存泄露:

未清除DOM的引用:

var refA = document.getElementById('refA');document.body.removeChild(refA);// refA不能回收,因此存在变量refA对它的引用,虽然移除了refA节点,但依然无法回收// 解决方案refA = null;

DOM对象添加的属性是一个对象的引用:

var myObj = {};document.getElementById('myDiv').myPro = myObj;// 解决方案,在页面onunload事件中释放document.getElementById('myDiv').myPro = null;

给DOM对象绑定事件:

var btn = document.getElementById("myBtn");btn.onclick = function(){    // 虽然最后把btn这个DOM移除,但是绑定的事件没有被移除,也会引起内存泄露,需要清除事件    // btn.onclick = null;    document.getElementById("mydiv").innerHTML = "zeronetwork";}// 其他document.body.removeChild(btn);btn = null
8457b9723774e79a12d4b66d3e6116c9.png

Web前端开发之Javascript-零点程序员-王唯

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值