网站首页 > 知识剖析 正文
本内容是《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(); // red
sayColor.call(this); // red
sayColor.call(window); // red
sayColor.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)); // 15
console.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和y
console.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 = 2
console.log(fun(2,3)); // 6
console.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值是同一个this
function 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
- 上一篇: 微信小程序学习笔记:Page()(小程序 page)
- 下一篇: 前端埋点统计方案思考(前端数据埋点)
猜你喜欢
- 2024-11-17 Android对so体积优化的探索与实践
- 2024-11-17 JavaScript对象(javascript对象主要包括)
- 2024-11-17 前端埋点统计方案思考(前端数据埋点)
- 2024-11-17 微信小程序学习笔记:Page()(小程序 page)
- 2024-11-17 广州蓝景分享—搞懂js事件、事件流(捕获冒泡)、事件委托
- 2024-11-17 程序员都必掌握的前端教程之JavaScript基础教程(下)
- 2024-11-17 DOM核心内容(dom的核心部分包括)
- 2024-11-17 小程序生命周期解析(从概念、启动、运行、销毁场景的全面解析)
- 2024-11-17 微信小程序学习笔记(二)——开发之框架
- 2024-11-17 《微信小程序开发从入门到实战》学习六十一
- 04-29php开发者composer使用看这一篇就够了
- 04-29引用和变量声明在不同语言中的实作
- 04-29PHP 没你想的那么差
- 04-29Ubuntu linux 上的 Nginx 和 Php 安装
- 04-29CentOS下通过yum搭建lnmp(单版本PHP)
- 04-29为什么 PHP8 是个高性能版本
- 04-29PHP8函数包含文件-PHP8知识详解
- 04-29使用无参数函数进行命令执行
- 最近发表
- 标签列表
-
- xml (46)
- css animation (57)
- array_slice (60)
- htmlspecialchars (54)
- position: absolute (54)
- datediff函数 (47)
- array_pop (49)
- jsmap (52)
- toggleclass (43)
- console.time (63)
- .sql (41)
- ahref (40)
- js json.parse (59)
- html复选框 (60)
- css 透明 (44)
- css 颜色 (47)
- php replace (41)
- css nth-child (48)
- min-height (40)
- xml schema (44)
- css 最后一个元素 (46)
- location.origin (44)
- table border (49)
- html tr (40)
- video controls (49)