想要获取笔记的可以点击下面链接获取,或者私信我
13,闭包
13-1 闭包
闭包是指有权访问另一个函数作用域中的变量的函数;其本质就是在一个函数内部创建另一个内部函数;并且此内部函数被暴露在外部,其会导致原有作用域链不能被释放,造成内存泄露;
其有3个特性
-
函数嵌套函数;
-
读取函数内部变量;
-
持久性,即让局部变量始终保存在内存中;
读取函数内部变量:
JS
function a(){
var name = "wangwei";
return function(){
return name;
}
}
var b = a();
console.log(b());
些变量的值始终保持在内存中:
其表现就是在一个函数内返回一个函数;即内部函数访问了外部函数的变量,通过把返回的函数赋值给一个外部全局变量,即使外部函数执行完毕,但其内部函数还一直
存在,并且其访问的外部函数中的变量也一直存在;
JS
function a(){
var i = 0;
function b(){
console.log(++i);
}
return b;
}
var c = a();
c(); // 1
c(); // 2
c(); // 3
// 或
function f1(){
var n = 999;
nAdd = function(){
n++;
};
function f2(){
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
nAdd();
result(); // 1000
会产生内存消耗的问题:
JS
function fn(){
var n = 2;
return function(){
var m = 0;
return "n:" + (++n) + ",m:" + (++m);
}
}
var f = fn();
console.log(f()); // n:3,m:1
console.log(f()); // n:4,m:1
经典示例:定时器与闭包:
JS
for(var i=0;i<5;i++){
setTimeout(function(){
console.log(i + " ");
},500);
} // 5个5
修改:
使用闭包来保存变量i,将setTimeout放入立即执行函数中,将for循环中的循环值i作为参数传递;但此时,是500毫秒后,同时打印出1-5;如何实现每隔500毫秒依次输出1-4?修改:
JS
for(var i=0;i<5;i++){
(function(i){
setTimeout(function(){
console.log(i + " ");
},i*500);
})(i);
}
13-2 闭包的作用:
js
// 保护函数内的变量,实现封装,防止变量流入其他环境发生命名冲突;
// 在内存中维持一个变量,可以做缓存;
// 实现公有变量,如函数累加器;
js
// 累加器
function add(){
var count = 0;
function done(){
count++;
console.log(count);
}
return done;
}
var counter = add();
counter(); // 1
counter(); // 2
counter(); // 3
function func(){
var num = 100;
function a(){
num++;
console.log(num);
}
function b(){
num--;
console.log(num);
}
return [a,b];
}
var arr = func();
arr[0](); // 101
arr[1](); // 100
// 封装,私有变量
function playing(){
var item = "";
var obj = {
play: function(){
console.log("playing:" + item);
item = "";
},
push: function(myItem){
item = myItem;
}
};
return obj;
}
var player = playing();
player.push('football');
player.play(); // playing:football
13-3 闭包的副作用:
闭包只能取得包含函数中任何变量的最后一个值,所以多次调用,只能取相同的一个值;
function createFun(){
var result = new Array();
for(var i = 0; i<10; i++){
result[i] = function(){
return i;
}
}
return result;
}
var arr = createFun();
for(var i=0;i<arr.length;i++){
console.log(arr[i]());
}
可以使用立即执行函数强制让闭包的行为符合预期;
js
function createFun(){
var result = new Array();
for(var i = 0; i<10; i++){
result[i] = function(num){
return function(){
return num;
}
}(i);
}
return result;
}
var arr = createFun();
for(var i=0;i<arr.length;i++){
console.log(arr[i]());
}
一个经典的应用,在若干个DOM对象绑定事件,分别输出不同的内容,如:
js
window.onload = function(){
var ul = document.getElementsByTagName('ul')[0];
var lis = ul.getElementsByTagName('li');
for(var i=0; i<lis.length; i++){
lis[i].addEventListener('click',function(e){
// console.log(this.innerText); // 不同
console.log(i); // 全是 4
},false);
}
}
// 改成:
window.onload = function(){
var ul = document.getElementsByTagName('ul')[0];
var lis = ul.getElementsByTagName('li');
for(var i=0; i<lis.length; i++){
(function(j){
lis[j].addEventListener('click',function(e){
console.log(j); // 达到预期,值不同
},false)
})(i);
}
}
13-4 闭包中的this
js
// 在闭包中使用this对象也可能会导致一些问题;this对象是在运行时基于函数的执行环境绑定的,即this对象本身就指调用函数的对象;在全局环境中,this对象通常指向window,而当函数被作为某个对象的方法调用时,this就等于那个对象;不过,匿名函数的执行环境具有全局性,因此this对象通常指向window,但有时候,由于编写闭包的方式不同,这一点可能不会那么明显;
js
var name = "The Window";
// var object = {
// name: 'My object',
// getNameFunc: function(){
// return function(){
// return this.name;
// }
// }
// };
// 把object改成:
var object = {
name: 'My object',
getNameFunc: function(){
var that = this;
return function(){
return that.name;
}
}
}
console.log(object.getNameFunc()()); // The Window或myobject
arguments存在着同样的问题;如果想访问作用域中的arguments对象,必须将对该对象的引用保存到另一个闭包能够访问的变量中;有几种特殊情况下,this的值可能会意外的改变,如:
js
var name = "The Window";
var object = {
name:"My Object",
getName:function(){
return this.name;
}
};
alert(object.getName()); // My Object
alert((object.getName)()); // My Object
alert((object.getName=object.getName)()); // The Window
13-5 闭包中的内存泄露
由于闭包会携带包含它的函数的作用域,即会使函数内变量被保存在内存中,所以内存消耗很大;因此在退出函数前,将不用的变量删除;
js
function handler(){
var element = document.getElementById("someElement");
element.onclick = function(){
console.log(element.id);
}
}
// 改成
function handler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
console.log(id);
};
element = null;
}
13-6 闭包中的作用域链
js
function createComparison(propertyName){
return function(object1,object2){
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if(value1<value2){
return -1;
}else if(value1>value2){
return 1;
}else{
return 0;
}
}
}
当某个函数被调用时,会创建一个执行环境及相应的作用域链;然后,使用arguments和其他命名参数的值来初始化函数的活动对象;但在作用域链中,外部函
数的活动对象始终处于第二位,外部函数的函数的活动对象处于第三位...直到作为作用域链终点的全局执行环境;
在函数执行过程中,为读取和写入变量的值,就需要在作用域中查找变量,如:
js
function compare(value1,value2){
if(value1<value2){
return -1;
}else if(value1>value2){
return 1;
}else{
return 0;
}
}
var result = compare(3,7);
在另一个函数内部定义的函数会将包含函数的活动对象添加到它的作用域链中;因此,在createComparison()函数内部定义的匿名函数的作用域中,实际上将会包含外部函数createComparison()的活动对象;
js
var compare = createCompare("name");
var result = compare({name:"Nicholas"},{name:"Greg"});
compare=null;
说明:解除对匿名函数的引用,以便释放内容,即通知垃圾回收将其清除;
13-7 闭包的几个应用
通常在使用只有一个方法的对象的地方,都可以使用闭包;在实际场景中,这种情况特别常见,比如,有很多代码都是基于事件的:定义某种行为,然后将其添加到用户触发的事件之上,通常称为回调,这个回调就是为响应事件而执行的函数,它们其实绝大部分都是闭包;
js
<style>
body{font-size: 14px;} h1{font-size: 1.5em;} h2{font-size: 1.2em;}
p{font-size: 1em;}
</style>
<h1>Web前端开发</h1>
<h2>JavaScript</h2>
<p>零点程序员</p>
<p><a href="#" id="size-14">14</a> <a href="#" id="size-16">16</a>
<a href="#" id="size-18">18</a></p>
<script>
function makeSize(size){
return function(){
document.body.style.fontSize = size + 'px';
};
}
var size14 = makeSize(14);
var size16 = makeSize(16);
var size18 = makeSize(18);
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
document.getElementById('size-18').onclick = size18;
</script>
13-7-1 函数工厂
js
function add(x){
return function(y){
return x + y;
}
}
var add5 = add(5);
var add10 = add(10);
console.log(add5(3)); // 8
console.log(add10(8)); // 18
console.log(add(1)(2)); // 3
13-7-2 表单控件的提示
js
<p id="tips">有关提示</p>
<p>邮箱:<input type="text" id="email" /></p>
<p>用户名:<input type="text" id="name" /></p>
<p>年龄:<input type="text" id="age" /></p>
<script>
function showTip(tip){
document.getElementById('tips').innerHTML = tip;
}
function makeTip(tip){
return function(){
showTip(tip);
};
}
function setupTips(){
var tipText = [
{'id':'email', 'tip': '邮箱地址'},
{'id':'name', 'tip': '你的用户名'},
{'id':'age', 'tip': '你的年龄'},
];
for(var i=0;i<tipText.length; i++){
var item = tipText[i];
document.getElementById(item.id).onfocus = makeTip(item.tip);
}
}
setupTips();
</script>
13-7-3 使用匿名闭包
js
for(var i=0;i<tipText.length; i++){
(function(){
var item = tipText[i];
document.getElementById(item.id).onfocus = function(){
showTip(item.tip);
};
})();
}
用ES6的let关键词
还可以使用forEach遍历数组,为每个元素添加一个监听器
js
tipText.forEach(function(item){
document.getElementById(item.id).onfocus = function(){
showTip(item.tip);
}
});
14,函数,作用域,垃圾回收
14-1 私有变量
严格来讲,Javascript中没有私有成员的概念;所有对象属性都是公有的,但是有一个私有变量的概念;任何在函数中定义的变量,都可能被认为是私有变量,因为不能在函数的外部访问这些变量;
私有变量包括函数的参数、局部变量和在函数内部定义的其他函数;如:
js
function add(num1,num2){
var sum = num1+num2;
return sum;
}
可以把有权访问私有变量和私有函数的公有的方法称为特权方法(privilegedmethod),利用私有和特权成员,可以隐藏那些不应该被直接修改的数据;有两种在对象上创建特权方法的方式;第一种是在构造函数中定义特权方法,如:
js
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());
这种方式,因为每次调用构造函数都会重新创建其中的所有方法,这显然不是必须的,也是一个缺点,使用静态私有变量来实现特权方法就可以避免这个问题;
14-2 静态私有变量
通过在私有作用域中定义私有变量或函数,同样可以创建特权方法;
这个模式与在构造函数中定义特权方法的主要区别,就是在于构造函数中的私有变量和函数是由实例共享的;而特权方法是在原型上定义的,因此所有实例都使用同一个函数;而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用,如:
JS
(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());
以这种方式创建静态私有变量会让每个实例都没有自己的私有变量;到底是使用实例变量,还是静态私有变量,最终还是看具体的需求;
14-3 函数的属性和方法
因为函数是对象,所以函数也有属性和方法;如length属性;name属性,非标准,通过这个属性可以访问到函数的名字;
js
function show(a,b,c){console.log(arguments.length);}
console.log(show.name); // show
如果是使用new Function()定义的,会返回anonymous;如:
js
var show = new Function();
console.log(show.name); // anonymous
使用函数表达式也可以返回函数名字;
js
var show = function(){console.log("func")};
console.log(show.name); // show
14-3-1 caller属性
该属性保存着调用当前函数的函数的引用;如果是在全局作用域中调用当前函数,它的值为null;
js
function outer(){inner();}
function inner(){console.log(inner.caller);}
outer();
为了实现更松散的耦合,也可以通过arguments.callee.caller来访问相同的信息,如:
js
function inner(){console.log(arguments.callee.caller);}
js
// 注:当在严格模式下运行时,arguments.callee会导致错误;
// 注:在严格模式下,还有一个限制:不能为函数的caller属性赋值,否则导致错误;
14-3-2 prototype属性
js
// 在ES核心所定义的全部属性中,最有意思的就是prototype属性了,其表示函数的原型;对于ES中的引用类型来说,prototype是保存它们所有实例方法的真正所在;换句话说,诸如toString()和valueOf()等方法实际上都保存在prototype属性中,只不过是通过各自对象的实例访问罢了;在创建自定义引用类型以及实现继承时,prototype属性的作用是极为重要的;在ES中,prototype属性是不可枚举的,因此使用for-in无法发现;
14-3-3 apply()和call()
每个函数都包含这两个方法;这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值;
apply()方法接收两个参数:一个是在其中运行该函数的作用域,另一个是参数数组;其中第二个参数可以是Array的实例,也可以是arguments对象;如:
js
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()时,传递给函数的参数必须逐个列举出来,如:
js
function sum(num1,num2){return num1+num2;}
function callSum(num1,num2){
return sum.call(this,num1,num2);
}
alert(callSum(10,20));
其真正的apply()和call()作用是能够扩充函数赖以运行的作用域;如:
js
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
js
// 使用call()或apply()来扩充作用域的最大好处,就是对象不需要与方法有任何耦合关系;
// bind()方法:主要作用就是将函数绑定至某个对象;
// 语法:fun.bind(this,arg1,arg2,...);
该方法会创建一个新的函数,称为绑定函数,其可传入两个参数,第一个参数作为this,第二个及以后的参数则作为函数的参数调用;即调用新的函数会把原始的函数当作对象的方法来调用;如:
js
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方法,兼容性的做法:
js
function bind(f,o){
if(f.bind) return f.bind(o);
else return function(){
return f.apply(o,arguments);
}
}
为bind()方法传入参数,该参数也会绑定至this;这种应用是一种常见的函数式编程技术,也被称为"柯里化"(currying),如:
js
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属性是绑定函数的形参个数减去绑定实参的个数,即调用新函数时所期望的实参的个数,如:
js
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;如:
js
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
14-4 高阶函数
所谓高阶函数(higher-order function)就是操作函数的函数,它接收一个或多个函数作为参数,或者返回一个函数;如:
js
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()方法,如:
js
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]));
更常见的应用:
js
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
递归:
js
// 递归是指函数调用自己;
// 语法:
function f1(){
...
f1();
...
}
// 隐含递归:
function f1(){...; f2(); ...}
function f2(){...; f1(); ...}
通过递归打印出1-9的数值,如:
js
function printNum(n){
if(n>=1){
printNum(n - 1);
}
console.log(n);
}
printNum(9);
js
// 递归函数效率低,但有利于理解和解决现实问题;
// 递归函数的执行过程:第一阶段"回推",第二阶段"递推";
// 函数在适当的时候能结束递归,否则会进入死循环;
js
function test(n){
console.log("a" + n);
n++;
if(n<=5){
test(n);
}
console.log("b" + n);
}
test(1); // 12345665432
又如:
js
// 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));
阶乘:
js
function factorial(num){
if(num<=1){
return 1;
}else{
return num*factorial(num-1);
}
}
注:但是如果类似于以下的代码,就会出错:
js
var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4));
在这种情况下,如果函数内部可以使用arguments.callee就可以解决问题;其指向正在执行的函数的指针,因此可以用它来实现对函数的递归调用:
js
return num*arguments.callee(num-1);
但在严格模式下,不能通过访问arguments.callee,访问这个属性会导致错误;不过,可以通过使用命名函数表达式来达到相同的效果;如:
js
var factorial = (function f(num){
if(num<=1){
return 1;
}else{
return num*f(num-1);
}
});
var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4));
14-5 垃圾回收
js
// JS实现了垃圾自动回收处理机制,即,执行环境会负责管理代码执行过程中使用的内存,会自动分配、释放内存;在其他语言中,一般是手工跟踪内存的使用情况,比如C语言,开发人员可以显式的分配和释放系统的内存;但在JavaScript中,开发人员不用关心内存使用问题,所需内存的分配以及无用内存的回收完全实现了自动管理;其实现的原理是:找出那些不再继续使用的变量,然后释放其占用的内存;为此垃圾回收器会按照固定的时间间隔或在某个预定的收集时间,周期性地执行;
js
var a = "zero";
var b = "network";
a = b; // "zero" 所占空间被释放
14-5-1 变量的生命周期
js
// 无论哪种开发语言,其内存的生命周期几乎是一样的:分配内存空间-使用内存空间-释放空间;
// 函数中局部变量的正常生命周期:只在函数执行的过程中存在;而在这个过程中,会为局部变量在栈或堆内存上分配相应的空间,以便存储它们的值;当函数执行结束,局部变量就没有存在的必要了,因此可以释放它们的内存以供将来使用;在这种情况下,很容易判断变量是否还有存在的必要;但并非所有情况下都这么容易判断;垃圾收集器必须跟踪哪个变量有用哪个变量没有用,对于不再有用的变量打上标记,以备将来收回其占用的内存;用于标识无用变量的策略可能会因实现而异,但具体到浏览器中的实现,则通常有两个策略;
1-1 标记清除
js
// JavaScript中最常用的垃圾收集方式是标记清除(mark-and-sweep);当变量进入环境时,就将这个变量标记为"进入环境";
// 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
// 目前,各浏览器使用的都是标记清除的策略,只不过垃圾收集的时间间隔互相不同。
1-2 引用计数
js
// 另一种不太常见的垃圾收集策略叫做引用计数(reference counting);引用计数的含义是跟踪记录每个值被用的次数;当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1,如果同一个值又被赋给另一个变量,则该值的引用次数加1;相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1;当这个值的引用次数变成0时,则说明没有办法再访问这个值了;因而就可以将其占用的内存空间回收回来;这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的空间了。
Navigator3是最早使用引用计数策略的浏览器,但遇到了一个严重的问题:循环引用;即对象A中包含指向对象B的指针,而对象B中也包含一个指向对象A的引用,如:
js
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对象,就会存在循环引用的问题;如:
js
var element = document.getElementById("some_element");
var myObject = new Object();
myObject.element = element;
element.someObject = myObject;
由于存在这个循环引用,即使将示例中的DOM从页面中移除,其也永远不会被回收;
js
// 为了避免类似这样的循环引用问题,最好是在不使用它们的时候手工断开原生JavaScript对象与DOM元素之间的连接,如:
myObject.element = null;
element.someObject = null;
目前,IE早已把BOM和DOM对象都转换成了真正的JavaScript对象;这样,就避免了两种垃圾收集算法并存导致的问题,也消除了常见的内存泄漏现象;
1-3 管理内存
js
// 使用具备垃圾收集机制的语言编写程序,开发人员一般不必要操心内存管理的问题;但是,JavaScript在进行内存管理及垃圾收集时面临的问题还是与众不同;其中最主要的一个问题,就是分配给Web浏览器的可用内存数量通常要比分配给桌面应用程序的要少;这样做的目的主要是出于安全方面的考虑,目的是防止运行JavaScript的网页耗尽全部系统内存而导致系统崩溃;内存限制问题不仅会影响给变量分配内存,同时还会影响调用栈以及在一个线程中能够同时执行的语句数量。
因此,确保占用最少的内存可以让页面获得更好的性能;而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据;一旦数据不再有用,最好通过将其值设置为null来释放其引用,即解除引用(dereferencing),其适用于大多数全局变量和全局对象的属性;如:
js
function createPerson(name){
var localPerson = new Object();
localPerson.name = name;
return localPerson;
}
var globalPerson = createPerson("wangwei");
globalPerson=null; // 手工解除globalPerson的引用
js
// 注:解除一个值的引用并不意味着自动回收该值所占用的内存;解除引用的值作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。
// JS的自动内存管理存在一些问题,例如垃圾回收实现可能存在缺陷或者不足,因此,需要找到一个合适的解决方法;
14-6 内存泄露
Javascript的几种内存泄露:
14-6-1 全局变量
js
// 一个没有声明的变量,成为了一个全局变量;因此,要避免这种情况出现,或者使用严格模式;
14-6-2 循环引用
js
// 即:A引用B,B引用A,如此,其引用计数都不为0,所以不会被回收;
// 解决:手工将它们设为null;
14-6-3 闭包
js
// 闭包会造成对象引用的生命周期脱离当前函数的作用域,使用不当,会造成内存泄露;
14-6-4 延时器、定时器
js
// setInterval / setTimeout中的this指向的是window对象,所以内部定义的变量也挂载在了全局,if引用了someResource变,如果没有清除setInterval/setTimeout的话,someResource得不到释放;
js
var someResource = getData();
setInterval(function(){
var node = document.getElementById('Node');
if(node){
node.innerHTML = JSON.stringify(someResource);
}
},1000);
14-6-5 DOM引用的内存泄露
未清除DOM的引用:
js
var refA = document.getElementById('refA');
document.body.removeChild(refA);
// refA不能回收,因此存在变量refA对它的引用,虽然移除了refA节点,但依然无法回收
// 解决方案
refA = null;
DOM对象添加的属性是一个对象的引用:
js
var myObj = {};
document.getElementById('myDiv').myPro = myObj;
// 解决方案,在页面onunload事件中释放
document.getElementById('myDiv').myPro = null;
给DOM对象绑定事件:
js
var btn = document.getElementById("myBtn");
btn.onclick = function(){
// 虽然最后把btn这个DOM移除,但是绑定的事件没有被移除,也会引起内存泄露,需要清除事件
// btn.onclick = null;
document.getElementById("mydiv").innerHTML = "zeronetwork";
}
// 其他
document.body.removeChild(btn);
btn = null
15,对象和构造函数
15-1 理解对象
可以创建一个最简单的自定义对象,就是使用Object,然后再为它添加属性和方法,如:
JS
var person = new Object();
person.name = "wangwei";
person.age = 18;
person.jog = "Engineer";
person.sayName = function(){
alert(this.name);
}
在多种场景中,常用对象字面量创建对象,如:
js
var person = {
name:"wangwei",
age:18,
job:"Engineer",
sayName:function(){
alert(this.name);
}
};
15-2 对象中的this
js
// 当一个函数作为对象的属性存在时,并且通过对象调用这个方法,那么函数中的this就指向调用函数的对象;
// this的好处在于,可以更加方便的访问对象内部成员;
15-3 早绑定和晚绑定
js
绑定:// 把对象的成员与对象实例结合在一起的方法。
早绑定:
// 指在实例化对象之前定义它的属性和方法,这样编译器或解释程序就能够提前转换机器代码。ES不是强类型语言,所以不支持早绑定;
晚绑定:
// 编译器或解释程序在运行前,不知道对象的类型。使用晚绑定,无需检查对象的类型,只需检查对象是否支持属性和方法即可;ES中的所有变量都采用晚绑定方法;这样就允许执行大量的对象操作;
15-4 属性访问错误
属性访问并不总是返回或设置一个值,如果访问一个不存在的属性并不会报错,会返回undefined;但如果试图访问一个不存在的对象的属性就会报错;null和undefined值是没有属性的,因此,访问这两个值的属性就会报错,如:
js
var book = {};
console.log(book.subtitle); // undefined
// console.log(book.subtitle.length); // 异常
// 解决方案
var len = book && book.subtitle && book.subtitle.length;
console.log(len); // undefined,不会报错
有些属性是只读的,不能重新赋值,有一些对象不允许新增属性,但如果操作这些属性,也不会报错,如:
js
// 内置构造函数的原型是只读的
// 赋值失败,但没有报错,Object.prototype没有修改
Object.prototype = 0;
这是历史遗留问题,但在严格模式下会抛出异常;
15-5 删除属性
delete删除对象的属性,但只是断开属性和宿主对象的联系,而不会去操作属性中的属性;
js
var a = {p:{x:1}};
var b = a.p;
delete a.p;
console.log(b.x); // 1
删除的属性的引用还存在,因此在某些实现中,有可能会造成内存泄漏;因此,在销毁对象时,要遍历属性中的属性,依次删除;
delete删除成功或者没有任何副作用时,它返回true;或者删除的不是一个属性访问表达式,同样返回true,如:
js
var o = {x:1};
delete o.x;
delete o.x;
console.log(delete o.toString); // 什么也没做,toString是继承来的
console.log(delete 1) // 无意义
// delete不能删除那些可置性为false的属性,
某些内置对象的属性是不可配置的,比如通过变量声明和函数声明创建的全局对象的属性;在严格模式下,删除一个不可配置属性会报一个类型错误,在非严格模式中,这些操作会返回false,如:
js
console.log(delete Object.prototype);// 不能删除,属性是不可配置的
var x = 1;
console.log(delete this.x); // 不能删除
function f(){}
console.log(delete this.f); // 不能删除
在非严格模式中,删除全局对象的可配置属性时,可以省略对全局对象的引用,但在严格模式下会报错,如:
json
"use strict";
this.x = 1;
console.log(delete this.x);
console.log(delete x); // 严格模式下异常
因此,必须显式指定对象及其属性;
虽然Object构造函数或对象字面量都可以用来创建单个对象,但这些方法有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码;为解决这个问题,可以使用工厂模式的方式创建对象;
15-6 工厂模式
工厂模式是软件工程领域一种广泛使用的设计模式,其抽象了创建具体对象的过程(还有其他设计模式);在ES中无法创建类,所以就发明了一种函数,用该函数来封装特定接口创建对象的细节,如:
js
function createPerson(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(o.name);
};
return o;
}
var p1 = createPerson("wangwei",18,"Engineer");
var p2 = createPerson("wujing",28,"doctor");
alert(p1.name);
alert(p2.name);
在工厂函数外定义对象方法,再通过属性指向该方法;
js
// 在上面的代码中改
function sayName(){
alert(this.name);
}
// 在原来的o.sayName = function(){...}改成如下
o.sayName = sayName;
15-7 构造函数
js
// 可以使用构造函数来创建特定类型的对象,如:Object和Array这种原生构造函数,在运行时会自动出现在执行环境中;
// 构造函数内能初始化对象,并返回对象;
// 此外,也可以创建自定义的构造函数,从而自定义对象类型的属性和方法;使用此种方式的目的:更加类似真正的面向对象创建(类)对象方法,也就是首先创建类;如:
js
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var p1 = new Person("wangwei",18,"Engineer");
var p2 = new Person("wujing",28,"Doctor");
alert(p1.name);
alert(p2.name);
// 这里的Person本身就是函数,只不过可以用来创建对象而已;
要创建实例对象,必须使用new实例化对象;以这种方式调用函数实际上会经历经下4个步骤:
js
// 创建一个新对象;
// 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
// 执行构造函数中的代码(为这个新对象添加属性);
// 返回新对象,即隐式的返回了this;
15-7-1 关于构造函数的返回值
js
// 先使用this,再使用o
function Person(name,age){
var o = {};
o.name = name;
o.age = age;
return o;
}
var p = new Person("wangwei",18);
console.log(p);
但如果返回是一个原始值,如:return 100,此时无任何影响,说明构造函数内返回的一定是一个对象;
在构造函数内还可以使用闭包:
js
function Person(name,age){
var money = 100;
this.name = name;
this.age = age;
function show(){
money ++;
console.log(money);
}
this.say = show;
}
var p1 = new Person();
p1.say();
p1.say();
var p2 = new Person();
p2.say();
15-7-2 constructor(构造函数)属性
实例都有一个constructor(构造函数)属性,该属性指向Person;
即:构造函数方式创建的实例有constructor(构造函数)属性,该属性指向类函数,如:
js
alert(p1.constructor == Person);
alert(p2.constructor == Person);
对象的constructor属性最初是用来标识对象类型的。但检测对象类型,instanceof操作符更可靠;
js
alert(p1 instanceof Object);
alert(p1 instanceof Person);
15-7-3 构造函数的特点
构造函数与其他函数的唯一区别:就在于调用它们的方式不同;构造函数也是函数,不存在定义构造函数的特殊语法;
任何函数,只要通过new操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new操作符调用,就是一个普通函数,如:
js
// 当作构造函数使用
var p1 = new Person("wangwei",18,"Engineer");
p1.sayName();
// 当作普通函数调用
Person("wujing",28,"Doctor");
window.sayName();
// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o,"Hello",38,"Worker");
o.sayName();
15-7-4 构造函数的缺点
这种方式虽然比较方便好用,但也并非没有缺点;缺点是:每个方法都要在每个实例上重新创建一遍,如sayName()方法,每个实例拥有的sayName(),但都不是同一个Function实例,如:
js
alert(p1.sayName == p2.sayName); // false
在ES中的函数是对象,因此每定义一个函数,也就实例化了一个对象,从逻辑上说,相当于:
js
this.sayName = new Function("alert(this.name)");
以这种方式创建函数,会导致不同的作用域链和标识符解析;但创建Function新实例的机制仍然是相同的;
可以把函数定义在构造函数外部;如:
js
function sayName(){
alert(this.name);
}
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
// 当作构造函数使用
var p1 = new Person("wangwei",18,"Engineer");
var p2 = new Person("wangwei",18,"Engineer");
alert(p1.sayName == p2.sayName); // true
16,原型prototype
16-1 原型prototype
js
// 创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,其指向一个原型对象,该对象的用途是:包含由特定类型创建的所有实例共享的属性和方法;
// 每个对象都从原型继承属性,也就是说,如果一个函数是一个类的话,这个类的所有实例对象都是从同一个原型对象上继承成员;因此,原型是类的核心;
优点:可以让所有对象实例共享它所包含的属性和方法;如:
js
function Person(){}
Person.prototype.name = "wangwei";
Person.prototype.age = 18;
Person.prototype.job = "Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var p1 = new Person();
p1.sayName();
var p2 = new Person();
p2.sayName();
alert(p1.sayName == p2.sayName);
js
// 所有通过对象字面量创建的对象都具有同一个原型对象,并可以通过Object.prototype获取对原型对象的引用;
// 通过new构造函数创建的对象,其原型就是构造函数的prototype属性;
// 同样,通过new Array()创建的对象的原型就是array.prototype,通过new Date()创建的对象的原型就是Date.prototype;
// 并不是所有的对象都具有原型,比如:Object.prototype本身就是一个对象,它就没有原型,并且也不继承任何属性;
对于一个实例对象来说,该实例的内部将包含一个指针,指向构造函数的原型对象;ES把这个指针称为[[Prototype]],但在脚本中,没有标准的方式访问[[Prototype]],但在很多实现中,每个对象上都支持一个属性__proto__,其就指向原型对象,并且可以通过脚本访问到;
js
function Person(name,age){}
console.log(Person.prototype);
var p = new Person("wangwei",18);
console.log(p.__proto__);
console.log(Person.prototype === p.__proto__);
构造函数的prototype属性被用作新对象的原型;这意味着通过同一个构造函数创建的所有对象,都继承自一个相同的对象,因此它们都是同一个类的成员;
16-2 构造函数和类的标识
js
// 原型对象是类的唯一标识,当且仅当两个对象继承自同一个原型对象时,它们才是属于同一个类的实例;
// 而初始化对象的状态的构造函数则不能作为类的标识,两个构造函数的prototype属性可能指向同一个原型对象,那么这两个构造函数创建的实例是属于同一个类的;
16-3 constructor属性
任何Javascript函数都可以用作构造函数,并且调用构造函数是需要用到prototype属性的;
在默认情况下,所有原型对象都会自动获得一个constructor属性,该属性是一个指向prototype属性所在函数的指针,其也是prototype属性中的唯一不可枚举属性,它的值就是一个函数对象;如
Person.prototype.constructor指向Person;如:
js
var F = function(){}; // F是函数对象
var p = F.prototype; // 这是F相关联的原型对象
var c = p.constructor; // 这是与原型相关联的函数
console.log(c === F); //true
可以看到构造函数的原型中存在预先定义好的constructor属性,该属性指向对象的构造函数;由于构造函数是类的"公共标识",因此constructor属性为对象提供了类;
js
var o = new F();
console.log(o.constructor === F); // true
创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性,至于其他方法,则都是从Object继承而来的;
上面的Range()构造函数,使用它自身的一个新对象重写预定义的Range.prtotype对象,这个新定义的原型对象不含有constructor属性,因此Range类的实例也不含有constructor属性,可以显式的为原型添加一个构造函数,如:
js
// 在Range.prototype中添加
constructor: Range, // 显式设置构造函数反向引用
// 设置了constructor,可以继续为原型对象添加其他属性和方法。
// 修改原例
Range.prototype.includes = function(x){return this.from <=x && x <= this.to;};
Range.prototype.foreach = function(f){
for(var x = Math.ceil(this.from); x <= this.to; x++) f(x);
};
Range.prototype.toString = function(){return "(" + this.from + "..." + this.to + ")";}
isPrototypeOf()方法:虽然在所有实现中都无法访问到[[Prototype]],但可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系;从本质上讲,如果[[Prototype]]指向调用isPrototypeOf()方法的对象,那么这个方法就返回true,如:
js
var p = {x:1};
var o = Object.create(p);
console.log(p.isPrototypeOf(o));
console.log(Person.prototype.isPrototypeOf(p1));
Object.getPrototypeOf():该方法返回[[Prototype]]的值,即查询一个对象的原型,如:
alert(Object.getPrototypeOf(p1) == Person.prototype); // true
alert(Object.getPrototypeOf(p1).name); // wangwei
alert(Object.getPrototypeOf(p1));
js
// 当代码读取某个对象的某个属性时,会执行一次搜索,目标是具有给定名字的属性;
// 搜索首先从对象实例本身开始,再到原型对象;如果在实例中找到了具有给定名字的属性,则返回该属性值,如果没找到,则继续搜索指针指向的原型对象,如果找到,就返回该属性值;也就是说,前面调用p1.sayName()时,会先后执行两次搜索;其次,p2也是如此;正因为如此,多个对象实例才能共享原型中所保存的属性和方法。
虽然可以通过对象实例访问保存在原型中的值,但不能通过对象实例重写原型中的值;如果在实例中添加一个属性,而该属性与原型中的一个属性同名,那么就会在该实例中创建该属性,该属性会屏蔽原型中的同名属性;
js
// 在以上的示例中添加
var p1 = new Person();
var p2 = new Person();
p1.name = "wujing";
alert(p1.name); // wujing 来自实例
alert(p2.name); // wangwei 来自原型
如果想恢复访问原型中的属性,默认情况下是恢复不了的,即使将实例属性设置为null,也不会恢复其指向原型的连接,但可以使用delete完全删除实例属性,从而能够重新访问原型中的属性,如:
js
// p1.name = null;
delete p1.name;
alert(p1.name); // wangwei 来自原型
16-4 重写整个原型
为了减少不必要的输入,也为了从视觉上更好的封装原型的功能,常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象;
JS
function Person(){}
Person.prototype = {
name:"wangwei",
age:18,
sayName:function(){
alert(this.name);
}
};
此时,instanceof操作还能返回正确的结果,但通过constructor已经无法确定对象的类型了。
js
var p = new Person();
alert(p instanceof Person); // true
alert(p instanceof Object); // true
alert(p.constructor == Person); // false
alert(p.constructor == Object); // true
如果需要constructor属性,可以在代码块中显式声明;
js
Person.prototype = {
constructor:Person,
name:"wangwei",
age:18,
sayName:function(){
alert(this.name);
}
};
var p = new Person();
alert(p.constructor);
alert(p.constructor == Person); // true
但是,以这种方式重设constructor属性会导致它的[[Enumerable]]特性被设置为true;默认情况下,原生的constructor属性是不可枚举的;可以通过Object.defineProperty()方法重设constructor,如:
js
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
var p = new Person();
alert(p.constructor);
alert(p.constructor == Person); // true
16-5 Object.create()
Object.create()方法规范了原型式继承,这个方法接收两个参数:一个用作新对象原型的对象和一个为新对象定义额外属性的对象(可选的);如:
js
var person = {
name:"wangwei",
friends:["wujing","lishi"]
};
var p1 = Object.create(person); // person作为原型对象传入,p1继承了person属性
console.log(p1);
console.log(person);
p1.name = "wujing";
p1.friends.push("adu");
console.log(p1);
var p2 = Object.create(person);
console.log(p2);
p2.name = "juanzi";
p2.friends.push("van");
console.log(p1.friends); //wujing,lishi,adu,van
console.log(person.friends); //wujing,lishi,adu,van
如果传入参数null,就会创建一个没有原型的新对象,其也不会继承任何成员,可以对它直接使用in运算符,而无需使用hasOwnProperty()方法,如:
js
var o = Object.create(null);
console.log(o); // No properties
如果想创建一个普通的空对象,比如通过{ }或new Object()创建的对象,需要传入Object.prototype,如:
js
var o = Object.create(Object.prototype);
console.log(o); // 与{}和new Object()一样
可以通过任意原型创建新对象,即可以使任意对象可继承,这是一个强大的特性;
Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同,每个属性都是通过自己的描述符定义的;以这种方式指定的任何属性都会覆盖原型对象上的同名属性;如:
js
var person = {
name:"wangwei",
friends:["wujing","lishi"]
};
var p1 = Object.create(person, {
name:{
value: "wujing"
}
});
console.log(p1.name);
16-6 原型的动态性
ES中基于原型的继承机制是动态的:对象从其原型继承属性,如果创建对象之后原型的属性发生改变,也会影响到继承这个原型的所有实例对象;这意味着可以通过给原型对象添加新方法来扩充ES类,即使先创建了实例后再修改原型也如此;如:
js
function Person(){}
Person.prototype = {
name:"wangwei",
age:18,
sayName:function(){
alert(this.name);
}
};
尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果重写整个原型对象,本质上就不一样了;如:
js
var p = new Person();
alert(p instanceof Person); // true
alert(p instanceof Object); // true
alert(p.constructor == Person); // false
alert(p.constructor == Object); // true
重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型;
16-7 原生对象的原型
原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,也是采用这种模式创建的,所有原生引用类型(Array, Object, String等),都在其构造函数的原型上定义了方法,如:Array.prototype中可以找到sort方法,在String.prototye中找到substring() 方法;如:
js
Person.prototype = {
constructor:Person,
name:"wangwei",
age:18,
sayName:function(){
alert(this.name);
}
};
var p = new Person();
alert(p.constructor);
alert(p.constructor == Person); // true
通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法,可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随时添加方法;
js
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
var p = new Person();
alert(p.constructor);
alert(p.constructor == Person); // true
可以给Object.prototype添加方法,从而使所有的对象都可以调用这些方法;尽管可以这么做,但不建议修改原生对象的原型,因为当在另一个支持该方法的实现中运行代码时,就可能会导致命名冲突,另外,这样做也可能会无意地重写原生方法,如果有必要添加的话,最好使用Object.defineProperty()方法进行属性的特性的定义;
16-8 原型对象的问题
原型缺点:首先其忽略了为构造函数传递初始化参数这一环节,所以在默认情况下所有实例将取得相同的属性值;其次,由于其属性是所有实例共享,对于基本类型的属性没有影响,毕竟通过在实例上添加一个同名属性,可以隐藏原型中的对应属性;但对引用类型的属性有影响;如:
js
function Person(){}
Person.prototype = {
constructor:Person,
name:"wangwei",
age:18,
friends: ["she","who"],
sayName:function(){
alert(this.name);
}
};
var p1 = new Person();
var p2 = new Person();
p1.friends.push("van"); // 但是如果p1.friends = []就不一样了,相当于新建了数组
alert(p1.friends); // she,who,van
alert(p2.friends); // she,who,van
alert(p1.friends === p2.friends); // true
16-9 组合使用构造函数模式和原型模式
创建自定义类型的最常见的方式,就是组合使用构造函数模式和原型模式;即用构造函数定义对象的实例属性,而用原型方式定义对象的方法和共享的属性;另外,这种混合模式还支持向构造函数传递参数;如:
js
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["she","who"];
}
Person.prototype = {
constructor: Person,
sayName: function(){
alert(this.name);
}
};
var p1 = new Person("wangwei",18,"Engineer");
var p2 = new Person("wujing",28,"Doctor");
p1.friends.push("van");
alert(p1.friends);
alert(p2.friends);
alert(p1.friends === p2.friends);
alert(p1.sayName === p2.sayName);
总结:这种构造函数模式与原型模式的混合,是目前使用最广泛,认同度最高的一种创建自定义类型的方法;可以说,这是用来定义引用类型的一种默认模式。
16-10 动态原型模式
原型模式还不是像其他语言一样把所有属性和方法都封装起来;
动态原型模式的基本思想是把所有信息封装到构造函数中,而通过构造函数初始化原型,又保持了同时使用构造函数和原型的优点;换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型;如:
js
function Person(name,age,job){
// 属性
this.name = name;
this.age = age;
this.job = job;
this.friends = ["she","who"];
// 方法
if (typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
var p = new Person("wangwei",18,"Engineer");
p.sayName();
这段代码只会在初次调用构造函数时才会执行;此时,原型已经完成初始化,不需要再做什么修改了;注意:这里对原型的所做的修改,能够立即在所有实例中得到反映;因此,这种方式比较完美;其中,if语句检查的可以是初始化之后应该存在的任何属性或方法,不必用很多的if语句检查每个属性和方法,只要检查其中一个即可;对于采用这种模式创建的对象,还可以使用instanceof操作符确定它的类型。
js
function Car(sColor,iDoors,iMpg){
this.color = sColor;
this.doors = iDoors;
this.mpg = iMpg;
this.drivers = new Array("Mike","Sue");
if(typeof Car._init == "undefined"){
Car.prototype.showColor = function(){
alert(this.color);
};
Car._init = true;
alert("ready");
}
}
var oCar1 = new Car("red",4,23); // ready
var oCar2 = new Car("blue",3,24); // 无,只执行一次
alert(oCar1 instanceof Car);
16-11 寄生构造函数模式(混合工厂)
基本思想:创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面看,这个函数又很像是典型的构造函数,如:
js
function Person(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.jog = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var p = new Person("wangwei",18,"Engineer");
p.sayName();
这种模式可以在特殊的情况下用来为对象创建构造函数,如创建一个具有额外方法的特殊数组,由于不能直接修改Array构造函数,因此可以使用这个模式,如:
function SpecialArray(){
var values = new Array(); // 创建数组
values.push.apply(values, arguments); // 添加值
values.toPipedString = function(){ // 添加方法
return this.join("|");
};
return values; // 返回数组
}
var colors = new SpecialArray("red","blue","green");
alert(colors.toPipedString()); // red|blue|green
16-12 稳妥构造函数模式(混合工厂)
js
// 道格拉斯Douglas Crockford发明了JS中的稳妥对象(durable objects)这个概念;
// 稳妥对象:指的是没有公共属性,而且其方法也不引用this的对象;
// 这种模式最适合在一些安全的环境中(会禁止使用this和new),或者在防止数据被其他程序修改时使用;
稳妥构造函数模式遵循与寄生构造函数类似的模式,但有两点不同:一是新创建的对象的实例方法不引用this; 二是不使用new操作符调用构造函数;如:
js
function Person(name,age,jog){
var o = new Object(); // 创建要返回的对象
// 可以在这里定义私有变量和函数
o.sayName = function(){
alert(name);
};
return o;
}
var p = Person("wangwei",18,"Engineer");
p.sayName();
16-13 小示例
字符串操作(提高其性能);
js
var str="hello";
str += "world";
var arr=new Array();
arr[0]="hello";
arr[1]="world";
var str=arr.join("");
可以把其封装,可复用:
js
function StringBuffer(){
this._strings = new Array();
}
StringBuffer.prototype.append=function(str){
this._strings.push(str);
}
StringBuffer.prototype.toString=function(){
return this._strings.join("");
}
var buffer=new StringBuffer();
buffer.append("hello");
buffer.append("world");
var str=buffer.toString();
document.write(str);
测试性能:
js
function StringBuffer(){
this._strings = new Array(); }
StringBuffer.prototype.append=function(str){
this._strings.push(str); }
StringBuffer.prototype.toString=function(){
return this._strings.join(""); }
var d1=new Date();
var str="";
for(var i=0; i<10000; i++){
str += "text"; }
var d2=new Date();
document.write("页面执行时间为:" + (d2.getTime() - d1.getTime()) + "<br>");
d1=new Date();
var buffer=new StringBuffer();
for(var i=0; i<10000; i++)
buffer.append("text");
str=buffer.toString();
d2=new Date();
document.write("页面执行时间为:" + (d2.getTime() - d1.getTime()) + "<br>");
利用prototype属性为所有对象自定义属性和方法;
js
//为Number对象添加输出16进制的方法;
Number.prototype.toHexString=function(){
return this.toString(16);
}
var iNum=15;
console.log(iNum.toHexString());
//为数组添加队列方法;
Array.prototype.enqueue=function(vItem){
this.push(vItem);
}
Array.prototype.dequeue=function(){
return this.shift();
}
var arr=new Array("red","blue","white");
arr.enqueue("green");
console.log(arr);
arr.dequeue();
console.log(arr);
//如果想给所有内置对象添加新方法,必须在Object对象的prototype属性上定义;
Object.prototype.showValue=function(){
console.log(this.valueOf());
};
var str="hello";
var iNum=23;
str.showValue();
iNum.showValue();
// 函数名只是指向函数的指针,因此可以使它指向其他函数
Function.prototype.toString=function(){
return "函数内部代码隐藏";
}
function say(){
console.log("hi");
}
console.log(say.toString());
//此方法会覆盖原始方法,所以可以在使用前存储它的指针,以便以后使用;
Function.prototype.oriToString=Function.prototype.toString;
Function.prototype.toString=function(){
if(this.oriToString().length>100){
return "内容过长,部分隐藏";
}else{
return this.oriToString();
}
}
16-14 遍历和枚举属性
16-14-1 in操作符
in操作符会通过对象能够访问给定属性时返回true,无论属性存在于实例中还是原型中:
js
alert(p1.hasOwnProperty("name")); // false
alert("name" in p1); // true
除了in操作符,更为便捷的方式是使用"!=="判断一个属性是否是undefined,如:
js
var o = {x:1};
o.x !== undefined; // true
o.y !== undefined; // false
o.toString != undefined; // true,o继承了toString
然而有一种场景只能使用in而不能使用上述属性访问的方式,in可以区分该属性存在但值为undefined的情景,如:
js
var o = {x:undefined};
o.x !== undefined //false,属性存在,但值为undefined
o.y !== undefined // false,属性不存在
"x" in o; // true,属性存在
"y" in o; // flase,属性不存在
delete o.x;
"x" in o; // false
在使用"!"时,要注意其与"!="不同点,"!"可以区分undefined和null,如:
js
var o={x:2};
// 如果o中存在属性x,且x的值不是null或undefined,则o.x乘以2
if(o.x != null) o.x *= 2;
// 如果o中存在属性x,且x的值不能转换为false,o.x乘以2
// 如果x是undefined、null、false、" "、0或NaN,则它保持不变
if(o.x) o.x *=2;
同时使用hasOwnProperty()方法和in操作符,可以确定该属性是存在于对象还是原型中;
js
// 判断是否为原型
var p1 = new Person();
// p1.name = "wujing"; // 添加此名就会返回false
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
alert(hasPrototypeProperty(p1,"name")); // true
// 或者:
var obj = {
name:"wang",
age:100,
sex:'male',
__proto__:{
lastName:"wei"
}
}
Object.prototype.height = '178CM';
for(var p in obj){
if(obj.hasOwnProperty(p)){
console.log(obj[p]);
}
}
除了检测对象的属性是否存在,还会经常遍历对象的属性;通常使用for/in循环遍历,但ES还提供了两个更好的方案;
for-in:可以遍历所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在原型中的属性;
js
var o = {x:1,y:2,z:3};
Object.defineProperty(o,"z",{value:4,enumerable:false});
console.log(o.z);
for(p in o)
console.log(p);
function Person(){this.name="wangwei";this.age=10;}
Person.prototype.toString = function(){return "wangwei";}
var p1 = new Person();
Object.defineProperty(p1,"age",{value:18,enumerable:false});
for(prop in p1)
console.log(prop + ":" + p1[prop]);
有时需要过滤遍历,如:
js
var obj={a:1,b:2};
var o = Object.create(obj);
o.x=1,o.y=2,o.z=3;
o.sayName = function(){}
for(p in o){
if(!o.hasOwnProperty(p)) continue; // 跳过继承的属性
console.log(p);
}
for(p in o){
if(typeof o[p] === "function") continue;
console.log(p); // 跳过方法
}
用来枚举属性的工具函数:
js
// 把p中的可枚举属性复制到o中,并返回o,如果o和p中含有同名属性,则覆盖
function extend(o,p){
for(prop in p)
o[prop] = p[prop];
return o;
}
// 如果o和p中有同名属性,则o中的属性不受影响
function merge(o,p){
for(prop in p){
if(o.hasOwnProperty[prop]) continue;
o[prop] = p[prop];
}
return o;
}
// 如果o和p中没有同名属性,则从o中删除这个属性
function restrict(o,p){
for(prop in o){
if(!(prop in p)) delete o[prop];
}
return o;
}
// 如果o和p中有同名属性,则从o中删除这个属性
function substract(o,p){
for(prop in p){
delete o[prop]; // 删除一个不存在的属性也不会报错
}
return o;
}
// 返回一个新对象,这个对象同时拥有o和p的属性,如果o和p有重名属性,则用p的属性
function union(o,p){
return extend(extend({},o), p);
}
// 返回一个新对象,这个对象同时拥有o和p的属性,交集,但p中属性的值被忽略
function intersection(o,p){
return restrict(extend({},o), p);
}
// 返回一个数组,这个数组包含的是o中可枚举的自有属性的名字
function keys(o){
if(typeof o !== "object") throw TypeError; // o必须为对象
var result = [];
for(var prop in o){ // 所有可枚举的属性
if(o.hasOwnProperty(prop)) // 判断是否是自有属性
result.push(prop);
}
return result;
}
16-15 Object.keys()方法
取得对象上所有可枚举的实例属性,该方法接受一个对象作为参数,返回一个包含所有可枚举属性的字符串数组,如:
js
var keys = Object.keys(Person.prototype);
alert(keys); // name,age,job,sayName,toString
var p1 = new Person();
p1.name = "wujing";
p1.age = 28;
keys = Object.keys(p1);
alert(keys); // name,age
16-16 Object.getOwnPropertyNames()方法
与Object.keys()类型,但获得是所有实例属性,无论它是否可枚举如:
var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys); // constructor,name,age,job,sayName,toString
分析说明:结果中包含了不可枚举的constructor属性;
js
注:Object.keys()和
Object.getOwnPropertyNames()方法都可以用来替代for-in循环。
17,类和模块化
17-1 Java式的类和对象
Java类和对象有以下成员:
实例字段:// 基于实例的属性或变量,用以保存独立对象的状态;
实例方法:// 是所有实例共享的方法,由每个独立的对象调用;
类字段:// 属于类的属性或变量,不是属于某个实例的;
类方法:// 属于类的方法,不属于某个实例的;
js
// Javascript与Java的一个不同之处在于,ES中的函数都是以值的形式出现的,方法和字段之间并没有太大的区别;如果属性值是函数,那么这个属性就定义了一个方法,否则,它只是一个普通的属性或字段;虽然有这些区别,但依然可以模拟出Java中的这四种成员类型;
// ES中的类牵扯三种不同的对象,这三种对象的属性的行为可以在一定程序上模拟Java的成员:
构造函数:
// 构造函数为ES的类定义了名字,任何添加到这个构造函数对象中的属性都是类字段和类方法(如果属性值是函数的话就是类方法);
原型:
// 原型对象的属性被所有实例所共享和继承,如果原型对象的属性值是函数的话,这个函数就作为实例的方法来调用;
实例:
// 类的每个实例都是一个独立的对象,直接给这个实例定义的属性是它专属的,如果该属性是函数,那它就是该实例的方法;
在ES中,定义类的步骤大概分为三步:
- 第一步,先定义一个构造函数,并设置初始化新对象的实例属性;
- 第二步,给构造函数的prototype对象定义实例的方法;
- 第三步,给构造函数定义类字段和类属性;
可以将这三步封装进一个deineClass()函数中,如:
js
function extend(o,p){
for(prop in p)
o[prop] = p[prop];
return o;
}
// 封装一个用以定义简单类的函数
function defineClass(constructor, // 用以设置实例的属性的函数
methods, // 实例的方法,复制到原型中
statics){ // 类属性,复制到构造函数中
if(methods) extend(constructor.prototype, methods);
if(statics) extend(constructor, statics);
return constructor;
}
// Range类的一个实现
var SimpleRange = defineClass(function(f,t){ this.f = f; this.t = t;},
{
includes: function(x) {return this.f <= x && x<=this.t;},
toString: function(){ return this.f + "..." + this.t;}
},
{upto: function(t){ return new SimpleRange(0, t);}});
封装一个表示复数的类,此示例体现了ES模拟Java式的类成员:
js
// Complex.js:表示复数的类
// 复数是实数和虚数的和,并且虚数i是-1的平方根
// 构造函数内的r和i,分别保存复数的实部和虚部,它们是对象的状态
function Complex(real, imaginary){
if(isNaN(real) || isNaN(imaginary)) throw new TypeError();
this.r = real;
this.i = imaginary;
}
// 当前复数对象加上另外一个复数,并返回一个新的计算和值后的复数对象
Complex.prototype.add = function(that){
return new Complex(this.r + that.r, this.i + that.i);
};
// 当前复数乘以另外一个复数,并返回一个新的计算乘积之后的复数对象
Complex.prototype.mul = function(that){
return new Complex(this.r*that.r - this.i*that.i, this.r*that.i + this.i*that.r);
};
// 计算复数的模,复数的模定义为原点(0,0)到复平面的距离
Complex.prototype.mag = function(){
return Math.sqrt(this.r*this.r + this.i * this.i);
}
// 复数的求负运算
Complex.prototype.neg = function(){
return new Complex(-this.r, -this.i);
};
// 将复数对象转换为一个字符串
Complex.prototype.toString = function(){
return "{" + this.r + "," + this.i + "}";
};
// 检测当前复数对象是否和另外一个复数值相等
Complex.prototype.equals = function(that){
return that != null &&
that.constructor === Complex &&
this.r === that.r && this.i === that.i;
};
// 定义静态类属性和方法,直接定义为构造函数的属性
// 它们只对其参数进行操作
// 先定义一些常量,以用在对复数运算中,当然也可把它们设为只读的
Complex.ZERO = new Complex(0,0);
Complex.ONE = new Complex(1,0);
Complex.I = new Complex(0,1);
// 这个类方法将实例对象的toString方法返回的字符串解析为一个Complex对象
Complex.parse = function(s){
try{
var m = Complex._format.exec(s);
return new Complex(parseFloat(m[1]), parseFloat(m[2]));
}catch(x){
throw new TypeError("Can't parse '" + s + "' as a complex number.");
}
}
// 定义私有属性,下划线表明它是类内部使用的,不属于类的公有API部分
Complex._format = /^\{([^,]+),([^}]+)\}$/;
// 应用
var c = new Complex(2,3);
var d = new Complex(c.i,c.r);
console.log(c.add(d).toString()); // {5,5}
var result = Complex.parse(c.toString()).// 将c转换为字符串,再转换为Complex对象
add(c.neg()). // 加上它的负数
equals(Complex.ZERO); // 结果应当永远是零
console.log(result); // true
尽管ES可以模拟出Java式的类成员,但Java中有很多重要的特性是无法在ES中模拟的;比如,对于Java类的实例方法来说,实例字段可以用做局部变量,并不需要使用this来引用它们,但ES是没有办法模拟这个特性的;
17-2 模块化
单例(singleton):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例;这样的类称为单例类;
单例其实有点类似于C# /C++里面的静态类;在ES中,是以对象字面量的方式来创建单例对象的;这样定义的对象,不能使用new的方式来生成另外的对象(因为不存在prototype和constructor属性)。
js
var singleton = {
name: value,
method: function(){
// 这里是方法的代码
}
};
增强模块模式(即包含私有成员的单例模式):
为单例创建私有变量和特权方法(公有方法),从而能增强单例的可访问性;
以模块模式定义的私有变量和私有函数只有单例对象本身的特权(公有)方法可以访问到,其他外部的任何对象都不可以;
其本质上就是使用闭包及匿名函数;
js
var singleton = function(){
var privateVar = 10; // 私有变量
function privateFun(){ // 私有函数
return false;
}
return { // 特权/公有方法和属性
publicProperty: true,
publicMethod: function(){
privateVar++;
return privateFun();
}
}
}();
从本质上来讲,这个对象字面量定义的是单例的公共接口;这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的,如:
js
function BaseComponent(){}
function OtherComponent(){}
var application = function(){
var components = new Array();
components.push(new BaseComponent());
return{
getComponentCount : function(){
return components.length;
},
registerComponent : function(component){
if(typeof component == "object"){
components.push(comment);
}
}
};
}();
application.registerComponent(new OtherComponent());
alert(application.getComponentCount());
增强的模块模式:
可以对模块模式进行增强,即在返回对象之前加入对其增强的代码;这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和方法对其加以增强的情况,如:
js
function CustomType(){}
var singleton = function(){
var privateVar = 10;
function privateFun(){
return false;
}
var object = new CustomType();
object.publicProperty = true;
object.publicMethod = function(){
privateVar++;
return privateFun();
};
return object;
}();
singleton.publicMethod();
如,前例中的application对象必须是BaseComponent的实例,修改如下:
function BaseComponent(){}
function OtherComponent(){}
var application = function(){
var components = new Array();
components.push(new BaseComponent());
var app = new BaseComponent();
app.getComponentCount = function(){
return components.length;
};
app.registerComponent = function(component){
if(component.constructor == BaseComponent){
components.push(component);
}else throw new Error("must be BaseComponent!");
};
return app;
}();
application.registerComponent(new OtherComponent()); // 异常
console.log(application.getComponentCount());
单例模式定义方式都是在定义时创建的单例,这样很浪费内存,可以使用惰性加载(lazy loading,更多的用于图片的延迟加载),即先定义,再在首次调用才创建对象;
js
var singleton = function() {
var unique;
return {
getinstance : function(){
if(!unique){
unique = new constructor();
return unique;
}
}
};
function constructor(){
var private_member = 10;
function private_method(){
console.log(private_member);
}
return { //这里才是真正的单例
public_member : "",
public_method : function(){
private_member++;
private_method();
}
};
}
}();
singleton.getinstance().public_method();
17-3 命名空间
js
// 将代码组织到类中的一个重要原因是,让代码显得更加"模块化",可以在很多不同场景中复用代码;但类不是唯一的模块化的方式,一般来说,模块是一个独立的JS文件;模块文件可以包含一个类定义、一组相关的类、一个实用函数库或者是一些待执行的代码;只要以模块的形式编写代码,任何JS代码段就可以当做一个模块;
// ES中并没有定义用以支持模块的语言结构,这也意味着在ES中编写模块化的代码更多的是遵循某一种编码约定;
// 很多JS库和客户端编程框架都包含一些模块系统,比如,Dojo工具包(包含了 Web 开发中的常用 JavaScript 工具)和Google的Closure库定义了provide()和require()函数,用以声明和加载模块;并且,CommonJS服务器端Javascript标准规范创建了一个模块规范,其同样使用了require()函数;这种模块系统通常用来处理模块加载和依赖性管理;如果使用这些框架,则必须按照框架提供的模块编写约定来定义模块;
// 模块化的目标是支持大规模的程序开发,处理分散源中代码的组装,并且能让代码正确运行,哪怕包含了所不期望出现的模块代码,也可以正确执行代码;为了做到这一点,不同的模块必须避免修改全局执行上下文,因此后续模块应当在它们所期望运行的原始(或接近)上下文中执行;这实际上意味着模块应当尽可能少地定义全局标识;理想的状况是,所有模块都不应当定义超过一个);
// 在模块创建过程中避免污染全局变量的一种方法是使用一个对象作为命名空间;它将函数和值作为命名空间对象属性存储起来(可以通过全局变量引用),而不是定义全局函数和变量;即体现了"保持干净的全局命名空间"的观点;
一个更好的方法是将类定义为一个单独的全局对象,如var sets = {},这个sets对象是模块的命名空间,并且将每个"集合"类都定义为这个对象的属性,如:
js
sets.SingletonSet = sets.AbstractEnumerableSet.extend(...);
如果想使用这样定义的类,需要通过命名空间来调用所需的构造函数,
js
如:
var s = new sets.SingletonSet(1);
// 模块与模块之间往往需要配合工作,因此,需要注意这种命名空间的用法带来的命名冲突;此时,可以使用所谓的"导入"功能,如:
var Set = sets.Set;
// 就会将Set导入到全局命名空间中,如:var s = new Set(1,2,3); 就可以这样使用了,而不必每次都要加sets;
有时,会使用更深层嵌套的命名空间,如果sets模块是另外一组更强大的模块集合的话,比如它的命名空间可能会是collections.sets,其代码会这样写:
js
var collections; // 声明(或重新声明)这个全局变量
if(!collections) // 如果它原本不存在
collections = {}; // 创建一个顶层的命名空间对象
collections.sets = {}; // 将sets命名空间创建在它的内部
// 在collections.sets内定义set类
collections.sets.AbstractSet = function(){//...}
最顶部的命名空间往往用来标识创建模块的作者或组织,并避免命名空间的命名冲突;比如,Google的Closure库在它的命名空间goog.structs中定义了Set类;每个开发者都会反转域名的组成部分,这样创建的命名空间前缀是全局唯一的,一般不会被其他模块作者采用;
如:
cn.zeronetwork.colloctions.sets
使用很长的命名空间来导入模块的方式很麻烦,可以将整个模块导入全局命名空间,而不是导入单独的类;如:
js
var sets = cn.zeronetwork.collections.sets;
按照约定,模块的文件名应当和命名空间匹配,sets模块应当保存文件sets.js中;如果这个模块使用命名空间collections.sets,那么这个文件应当保存在目录collections/下,比如:使用命名空间
cn.zeronetwork.collections.sets的模块应当在文件cn/zeronetwork/collections/sets.js中;
作为私有命名空间的函数:
模块对外导出一些公用API,这些API是提供给其他程序员使用的,它包括函数、类、属性和方法;但模块的实现往往需要一些额外的辅助函数和方法,这些函数和方法并不需要在模块外部可见;比如,前面的_v2s()函数,并不希望Set类的用户在某时刻调用这个函数,因此这个方法最好在类的外部是不可以访问的;
可以通过将模块定义在某个函数的内部来实现,即将这个函数作用域用做模块的私有命名空间(有时也称为模块函数),如下面的示例就是模块函数,如:
js
// 声明全局变量Set,使用一个函数的返回值给它赋值
// 需要立即执行,它的返回值将赋值给Set;
var Set = (function invocation(){
function Set(){ // 这个构造函数是局部变量
this.values = {};
this.n = 0;
this.add.apply(this, arguments);
}
// 给Set.prototype定义实例方法
Set.prototype.contains = function(value){
// 调用了v2s(),而不是调用带有前缀的set._v2s()
return this.values.hasOwnProperty(v2s(value));
};
Set.prototype.size = function(){return this.n;};
Set.prototype.add = function(){/** */ };
Set.prototype.remove = function(){/** */};
Set.prototype.foreach = function(f,c){/** */};
// 以下是上面的方法用到的一些辅助函数和变量
// 它们不属于模块的共有的API,但它们都隐藏在这个函数作用域内
// 因此不必将它们定义为Set的属性或使用下划线作为其前缀
function v2s(val){/** */};
function objectId(o){/** */};
var nextId = 1;
// 这个模块的共有API是Set()构造函数
// 需要把这个函数从私有空间中导出来,以便在外部使用它
return Set;
}());
其中函数名字invocation,用以强调这个函数应当在定义后立即执行;namespace用来强调这个函数被用作命名空间;
一旦将模块代码封装进一个函数,就需要一些方法导出其公用API,以便在模块函数的外部调用它们;模块函数返回构造函数,这个构造函数随后赋值给一个全局变量;将值返回已经清楚地表明API已经导出在函数作用域之外,如果模块API包含多个单元,则它可以返回命名空间对象,如:
js
function AbstractSet(){}
function NotSet(){}
function AbstractEnumerableSet(){}
function SingletonSet(){}
function AbstractWritableSet(){}
function ArraySet(){}
// 创建一个全局变量用来存放集合相关的模块
var collections;
if(!collections) collections = {};
// 定义sets模块
collections.sets = (function namespace(){
// 在这里定义多种集合类,使用局部变量和函数
// ...
// 通过返回命名空间对象将API导出
return {
// 导出的属性名:局部变量名字
AbstractSet: AbstractSet,
NotSet: NotSet,
AbstractEnumerableSet: AbstractEnumerableSet,
SingletonSet: SingletonSet,
AbstractWritableSet: AbstractWritableSet,
ArraySet: ArraySet
};
}());
var a = new collections.sets.AbstractSet();
console.log(a);
另外一种类似的方式是将模块函数当做构造函数,通过new来调用,通过将它的实例赋值给this来将其导出:
js
// 创建一个全局变量用来存放集合相关的模块
var collections;
if(!collections) collections = {};
// 定义sets模块
collections.sets = (new function namespace(){
// 在这里定义多种集合类,使用局部变量和函数
// ...
// 将API导出到this对象
this.AbstractSet = AbstractSet;
this.NotSet = NotSet
//...
// 注,这里没有返回值
}());
console.log(new collections.sets.AbstractSet());
作为一种替代方案,如果已经定义了全局命名空间对象,这个模块函数可以直接设置成那个对象的属性,不用返回任何内容,如:
js
var collections;
if(!collections) collections = {};
collections.sets = {};
(function namespace(){
// ...
// 将API导出到上面创建的命名空间对象上
collections.sets.AbstractSet = AbstractSet;
collections.sets.NotSet = NotSet
//...
// 导出的操作已经执行了,这里不需要再有返回值
}());
有些框架实现了模块加载功能,其中包括其他一些导出模块API的方法;比如,使用provides()函数来注册其API,提供exports对象用以存储模块API;由于JS目前还不具备模块管理的能力,因此应当根据所使用的框架和工具包来选择合适的模块创建和导出API的方式。
18,类型和对象
18-1 类属性
对象的类属性(class attribute)是一个字符串,用以表示对象的类型信息;ES5并没有提供设置这个属性的方法,并只有一种间接的方法可以查询它,即默认的toString(),比如,打印Object类型的对象时,返回[object Object],因此,想要获得对象的类,可以调用对象的toString方法,然后提取返回字符串的第8个到第二个位置之间的字符;
但是,很多对象继承的toString()方法重写了,为了能调用正确的toString()版本,必须间接地调用Function.call()方法,如:
JS
function classof(o){
if(o===null) return "Null";
if(o===undefined) return "Undefined";
return Object.prototype.toString.call(o).slice(8,-1);
}
console.log(classof(null));
console.log(classof(1));
console.log(classof(""));
console.log(classof(false));
console.log(classof({}));
console.log(classof([]));
console.log(classof(/./));
console.log(classof(new Date()));
console.log(classof(window));
function F(){}
console.log(classof(new F()));
classof()函数可以传入任何类型的参数;但是对自定义类型,返回的也是Object,也就是说通过类属性没办法区分自定义类型;
18-2 检测对象的类型
18-2-1 instanceof运算符
js
// 如果o继承自c.prototype,那表达式o instanceof c值为true;这里的继承可以不是直接继承,如果o所继承的对象继承自另一个对象,后一个对象继承自c.prototype,这个表达式的运算结果也是true;
// 构造函数是类的公共标识,但原型是唯一 的标识;尽管instanceof右操作数是构造函数,但计算过程实际上是检测了对象的继承关系,而不是检测创建对象的构造函数;
如果想检测对象的原型链上是否存在某个特定的原型对象,可以使用isPrototypeOf()方法,如:
js
rang.methods.isPrototypeOf(r); // rang.methods是原型对象
instanceof和isPrototpyeOf()方法的缺点时,无法通过对象来获得类名,只能检测对象是否属于指定的类名;在客户端Javascript中有一个比较严重的问题,就是在多窗口和多框架子页面的Web应用中兼容性不好;每个窗口和框架子页面都具有单独的执行上下文,每个上下文都包含独有的全局变量和一组构造函数;在两个不同框架页面中创建的两个数组继承自两个相同但相互独立的原型对象,其中一个框架页面中的数组不是另一个框架页面的Array()构造函数的实例,instanceof运算结果是false;
构造函数和类的标识:
原型对象是类的唯一标识,当且仅当两个对象继承自同一个原型对象时,它们才是属于同一个类的实例;
而初始化对象的状态的构造函数则不能作为类的标识,两个构造函数的prototype属性可能指向同一个原型对象,那么这两个构造函数创建的实例是属于同一个类的;
尽管构造函数不像原型那样基础,但构造函数是类的"外在表现";构造函数的名字通常用做类名,比如Person()构造函数创建Person对象;然而,更根本地讲,当使用instance运算符来检测对象是否属于某个类时会用到构造函数,如 p instanceof Person,但实际上instanceof运算符并不会检查p是否是由Person()构造函数初始化而来,而会检查p是否继承Person.prototype;
18-2-2 constructor属性
另一种识别对象是否属于某个类的方法是使用constructor属性,因为构造函数是类的公共标识,所以最直接的方法就是使用constructor属性,如:
js
function typeAndValue(x) {
if(x==null) return ""; // Null和undefined没有构造函数
switch(x.constructor){
case Number: return "Number: " + x;
case String: return "String: '" + x + "'";
case Date: return "Date: " + x;
case RegExp: return "RegExp: " + x;
case Complex: return "Complex: " + x;
}
}
使用constructor属性检测对象属于某个类的技术的不足之处和instanceof一样;在多个执行上下文的场景中它是无法正常工作的;
另外,在Javascript中也并非所有的对象都包含constructor属性;在每个新创建的函数原型上默认会有constructor属性,但我们经常会忽略原型上的constructor属性,比如前面的示例代码中所定义的两个类,它们的实例都没有constructor属性;
18-2-3 构造函数的名称
另一种检测的可能的方式 是使用构造函数的名字而不是构造函数本身作为类标识符;
两个不同窗口的Array构造函数是不相等的,但它们的名字是一样的;在一些Javascript的实现中为函数对象提供了一个非标准的属性name,用来表示函数的名称;对于那些没有name属性的Javascript实现来说,可以将函数转换为字符串,再从中提取出函数名,比如,下面的函数的getName()方法,就是使用这种方式取得函数名;如:
js
// 可以判断值的类型的type()函数
function type(o) {
var t, c, n; // type,class,name
if(o === null) return "null"; // 处理null值
if(o !== o) return "nan"; // NaN和它自身不相等
// 如果typeof的值不是object,则使用这个值,即可以识别出原始值和函数
if((t = typeof o) !== "object") return t;
// 返回对象的类型,除非值为Object,可以识别出大多数的内置对象
if((c = classof(o)) !== "object") return c;
// 如果对象构造函数的名字存在的话,则返回它
if(o.constructor && typeof o.constructor === "function" &&
(n = o.constructor.getName())) return n;
// 其他的类型都无法差断,一律返回Object
return "Object";
}
// 返回对象的类
function classof(o) {
return Object.prototype.toString.call(o).slice(8,-1);
}
// 返回函数的名字(可能是空字符串),不是函数的话,返回null
Function.prototype.getName = function (){
if("name" in this) return this.name;
return this.name = this.toString().match(/function\s*([^(]*)\(/)[1];
}
18-2-4 鸭式辩型
上面所描述的检测对象的类的各种技术多少都会有问题,至少在客户端Javascript中是如此;解决办法就是规避掉这些问题:不要关注"对象的类是什么",而是关注"对象能做什么";这种思考问题的方式 在Python和Ruby中非常普通,称为"鸭式辩型";
鸭式辩型,就是说检测对象是否实现了一个或多个方法;一个强类型的函数需要的参数必须是某种类型,而"鸭式辩型",只要对象包含某些方法,就可以作为参数传入;
鸭式辩型在使用时,需要对输入对象进行检查,但不是检查它们的类,而是用适当的名字来检查它们所实现的方法;
示例:quacks()函数用以检查一个对象(第一个参数)是否实现了剩下的参数所表示的方法:
js
// 利用鸭式辩型实现的函数
// 如果o实现了除第一个参数之外的参数所表示的方法,则返回true
function quacks(o /*, ...*/) {
for(var i=1; i<arguments.length; i++){ // 遍历o之后所有参数
var arg = arguments[i];
switch(typeof arg){
case 'string':
if(typeof o[arg] !== "function")
return false;
continue;
case 'function':
// 如果实参是函数,则使用它的原型
arg = arg.prototype; // 进行下一个case
case 'object':
for(var m in arg){ // 遍历对象的每个属性
// 跳过不是方法的属性
if(typeof arg[m] !== "function") continue;
if(typeof o[m] !== "function") return false;
}
}
}
// 如果程序能执行到这里,说明o实现了所有的方法
return true;
}
18-2-5 集合类
集合(set)是一种数据结构,用以表示非重复值的无序集合;集合的基础方法包括添加值、检测值是否在集合中,这种集合需要一种通用的实现,以保证操作效率;
一个更加通用的Set类,它实现了从Javascript值到唯一字符串的映射,将字符串用做属性名;对象和函数具备可靠的唯一字符串表示;因此集合类必须给集合中的每个对象或函数定义一个唯一的属性标识:
js
// Set.js 值的任意集合
function Set(){
this.values = {}; //集合数据保存在对象的属性里
this.n = 0; // 集合中值的个数
this.add.apply(this, arguments); // 把所有参数都添加进这个集合
}
// 把所有参数都添加进这个集合
Set.prototype.add = function(){
for(var i=0; i<arguments.length; i++){
var val = arguments[i];
var str = Set._v2s(val); // 把它转换为字符串
if(!this.values.hasOwnProperty(str)){ // 如果不在集合里
this.values[str] = val;
this.n++;
}
}
return this; // 支持链式调用
};
// 从集合删除元素,这些元素由参数指定
Set.prototype.remove = function(){
for(var i=0;i<arguments.length; i++){
var str = Set._v2s(arguments[i]);
if(this.values.hasOwnProperty(str)){
delete this.values[str];
this.n--;
}
}
};
// 如果集合包含该值,返回true,反之返回false
Set.prototype.contains = function(value){
return this.values.hasOwnProperty(Set._v2s(value));
};
// 返回集合的大小
Set.prototype.size = function(){
return this.n;
};
// 遍历集合中的所有元素,在指定的上下文中调用f
Set.prototype.foreach = function(f, context){
for(var s in this.values){
if(this.values.hasOwnProperty(s)) // 忽略继承的属性
f.call(context, this.values[s]);
}
};
// 内部函数,用以将任意Javascript值和唯一的字符串对应起来
Set._v2s = function(val){
switch(val){
case undefined: return 'u'; // 特殊的原始值
case null: return 'n'; // 只有一个字母代码
case true: return 't';
case false: return 'f';
default: switch(typeof val){
case 'number': return '#' + val; // 数字带#前缀
case 'string': return '"' + val; // 字符串带"前缀
default: return '@' + objectId(val); // 对象和函数带有@
}
}
function objectId(o){
var prop = "|**objectid**|"; // 私有属性,用于存放id
if(!o.hasOwnProperty(prop)){ // 如果对象没有id
o[prop] = Set._v2s.next++; // 将下一个值赋给它
}
return o[prop];
}
};
Set._v2s.next = 100; // 设置初始id的值
// 应用
var persons = new Set("a","b");
persons.add("c",1,null,{name:"wangwei"});
console.log(persons);
console.log(persons.contains("b"));
console.log(persons.size());
function show(v){
console.log("元素:" + v);
}
persons.foreach(show,window);
18-2-6 枚举类型-示例
枚举类型是一种类型,它是值的有限集合,如果值定义为这个类型则该值是可列出(可枚举)的;
以下的示例包含一个单独函数enumeration(),但它不是构造函数,它没有定义一个名叫enumeration的类,相反,它是一个工厂方法,每次调用它创建一个新的类,如:
js
// 使用4个值创建新的Coin类
var Coin = enumeration({Penny:1, Nickel:5, Dime:10, Quarter:25});
var c = Coin.Dime; // 这是新类的实例
console.log(c instanceof Coin); // true
console.log(c.constructor == Coin); // true
console.log(Coin.Quarter + 3 * Coin.Nickel); // 40,将值转换为数字
Coin.Dime == 10; // true, 更多转换为数字的例子
Coin.Dime > Coin.Nickel; // true
String(Coin.Dime) + ":" + Coin.Dime;
// 枚举类型
// 实参对象表示类的每个实例的名/值
// 返回一个构造函数,它标识这个新类
// 注:这个构造函数也会抛出异常,不能使用它来创建该类型的新实例
// 返回的构造函数包含名/值对的映射表
// 包括由值组成的数组,以及一个foreach()迭代器
function enumeration(namesToValues){
// 这个虚拟的构造函数是返回值
var enumeration = function(){throw "Can't Instantiate Enumerations";};
// 枚举值继承自这个对象
var proto = enumeration.prototype = {
constructor: enumeration, // 标识类型
toString: function(){ return this.name;}, // 返回名字
valueOf: function(){ return this.value;}, // 返回值
toJSON: function(){ return this.name;} // 转换为JSON
};
// 用以存放枚举对象的数组
enumeration.values = [];
// 创建新类型的实例
for(name in namesToValues){
var e = Object.create(proto); // 创建一个代表它的对象
e.name = name; // 给它一个名字
e.value = namesToValues[name]; // 给它一个值
enumeration[name] = e; // 将它设置为构造函数的属性
enumeration.values.push(e);
}
// 一个类方法,用来对类的实例进行迭代
enumeration.foreach = function(f,c){
for(var i=0; i<this.values.length; i++)
f.call(c, this.values[i]);
};
// 返回标识这个新类型的构造函数
return enumeration;
}
用以上定义的枚举类型实现一副扑克牌的类:
// 定义一个表示玩牌的类
function Card(suit, rank){
this.suit = suit; // 每张牌都有花色
this.rank = rank; // 点数
}
// 使用枚举类型定义花色和点数
Card.Suit = enumeration({Clubs:1, Diamonds: 2, Hearts: 3, Spades: 4});
Card.Rank = enumeration({Two:2, Three:3, Four:4, Five:5, Six:6, Seven:7,Eight:8,
Nine:9, Ten:10, Jack:11, Quee:12, King:13, Ace:14});
// 定义用以描述牌面的文本
Card.prototype.toString = function(){
return this.rank.toString() + " of " + this.suit.toString();
};
// 比较扑克牌中两张牌的大小
Card.prototype.compareTo = function(that){
if(this.rank < that.rank) return -1;
if(this.rank > that.rank) return 1;
return 0;
};
// 以扑克牌的玩法规则对牌进行排序的函数
Card.orderByRank = function(a, b){ return a.compareTo(b);};
// 以桥牌的玩法规则对牌进行排序的函数
Card.orderBySuit = function(a,b){
if(a.suit < b.suit) return -1;
if(a.suit > b.suit) return 1;
if(a.rank < b.rank) return -1;
if(a.rank > b.rank) return 1;
return 0;
};
// 定义用以表示一副标准扑克牌的类
function Deck(){
var cards = this.cards = []; // 一副牌就是由牌组成的数组
Card.Suit.foreach(function(s){ // 初始化这个数组
Card.Rank.foreach(function(r){
cards.push(new Card(s, r));
});
})
}
// 洗牌的方法:重新洗牌并返回洗好的牌
Deck.prototype.shuffle = function(){
// 遍历数组中的每个元素,随机找出牌面最小的元素,并与之(当前遍历的元素)交换
var deck = this.cards, len = deck.length;
for(var i = len-1; i>0; i--){
var r = Math.floor(Math.random()*(i+1)), temp; // 随机数
temp = deck[i], deck[i] = deck[r], deck[r] = temp; // 交换
}
return this;
}
// 发牌的方法:返回牌的数组
Deck.prototype.deal = function(n){
if(this.cards.lenght < n) throw "Out of cards";
return this.cards.splice(this.cards.length - n, n);
};
// 创建一副新扑克牌,洗牌并发牌
var deck = (new Deck()).shuffle();
var hand = deck.deal(13).sort(Card.orderBySuit);
console.log(deck);
console.log(hand);
18-2-7 标准转换方法
对象类型转换所用到的方法,其中有一些在进行转换时由Javascript解释器自动调用;
不需要为定义的每个类都实现这些方法,但这些方法的确非常重要,如果没有为自定义的类实现这些方法,也应当是有意为之,而不应该忽略掉它。
js
// toString()方法;这个方法的作用是返回一个可以表示这个对象的字符串;在希望使用字符串的地方用到对象的话,Javascript会自动调用这个方法;如果没有实现这个方法,类会默认从Object.prototype中继承toString()方法,这个方法的运算结果是"[object Object]",这个字符串用处不大;toString()方法应当返回一个可读的字符串,这样最终用户才能将这个输出值利用起来,然而有时候并不一定非要如此,不管怎样,可以返回可读字符串的toString()方法也会让后续的工作更加轻松;
// toLocaleString()和toString极为类似:toLocaleString()是以本地敏感性的方式来将对象转换为字符串;默认情况下,对象所继承的toLocaleString()方法只是简单地调用toString()方法;有一些内置类型包含有用的toLocaleString()方法用以返回本地化相关的字符;如果需要为对象以字符的转换定义toString()方法,那么同样需要定义toLocaleString()方法用以处理本地化的对象到字符串的转换;
// valueOf()方法,它用来将对象转换为原始值;比如,当数学运算符(除了"+")和关系运算符作用于数字文本表示的对象时,会自动调用 valueOf()方法;大多数对象都没有合适的原始值来表示它们,也没有定义这个方法;
// toJSON()方法,这个方法是由JSON.stringify()自动调用的;JSON格式用于序列化良好的数据结构,而且可以处理Javascript原始值、数组长和纯对象;它和类无关,当对一个对象执行序列化操作时,它会忽略对象的原型和构造函数;比如将Range对象或Complex对象作为参数传入JSON.stringify(),将会返回诸如{"from":1, "to":3}或{...}这种字符串;如果将这些字符串传入JSON.parse(),则会得到一个和Range对象或Complex对象具有相同属性的纯对象,但这个对象不会包含从Range和Complex继承来的方法;
// 这种序列化操作非常适用于诸如Range类和Complex这种类,但对于其他一些类则必须自定义toJSON方法来定制个性化的序列化格式;如果一个对象有toJSON方法,JSON.stringify()并不会对传入的对象做序列化操作,而会调用 toJSON()来执行序列化操作;比如,Date对象的toJSON()方法可以返回一个表示日期的字符;
对于一个集合,最接近JSON的表示方法就是数组;
js
// 将这些方法添加到Set类的原型对象中
extend(Set.prototype,{
// 将集合转换为字符
toString: function(){
var s = "{", i=0;
this.foreach(function(v){s += ((i++>0) ? ", " : "") + v;});
return s + "}";
},
// 类似toString,但是对于所有的值都将调用 toLocaleString()
toLocaleString: function(){
var s = "{", i=0;
this.foreach(function(v){
if(i++>0) s+= ", ";
if(v == null) s+=v; // null和undefined
else s+= v.toLocaleString(); // 其他情况
});
return s + "}";
},
// 将集合转换为值数组
toArray: function(){
var a = [];
this.foreach(function(v){ a.push(v);});
}
});
// 对于要从JSON转换为字符串的集合都被当做数组来对待
Set.prototype.toJSON = Set.prototype.toArray;
18-3 比较方法
Javascript的相等运算符比较对象时,比较的是引用而不是值;也就是说,给定两个对象引用,如果要看它们是否指向同一个对象,不是检查这两个对象是否具有相同的属性名和相同的属性值 ,而是直接比较这两个单独的对象是否相等,或者比较它们的顺序;
如果定义一个类,并且希望比较类的实例,应该定义合适的方法来执行比较操作;
Java语言有很多用于对象比较的方法,Javascript可以模拟这些方法;为了能让自定义类的实例具备比较的功能,定义一个名为equals()实例方法;这个方法只能接收一个实参,如果这个实参和调用此方法的对象相等的话则返回true;当然,这个相等的含义是根据类的上下文来决定的;
对于简单的类,可以通过简单地比较它们的constructor属性来确保两个对象是相同类型,然后比较两个对象的实例属性以保证它们的值相等,如:
js
// 重写它的constructor属性
Range.prototype.constructor = Range;
// 一个Range对象和其他不是Range的对象均不相等
// 当且仅当两个范围的端点相等,它们才相等
Range.prototype.equals = function(that){
if(that == null) return false;
if(that.constructor != Range) return false; // 处理非Range对象
return this.from == that.from && this.to == that.to;
};
给Set类定义equals()方法,如:
js
Set.prototype.equals = function(that){
if(this === that) return true; // 一些闪要情况的快捷处理
// 如果that对象不是一个集合,它和this不相等
// 用到了instanceof,使得这个方法可以用于Set的任何子类
// 如果希望采用鸭式辩型的方法,可以降低检查的严格程序
// 或者可以通过this.constructor == that.constructor来加强检查的严格程序
// 注,null和undefined两个值是无法用于instanceof运算的
if(!(that instanceof Set)) return false;
// 如果两个集合的大小不一样,则它们不相等
if(this.size() != that.size()) return false;
// 现在检查两个集合中的元素是否完全一样
// 如果两个集合不相等,则通过抛出异常来终止foreach循环
try{
this.forEach(function(v){ if(!that.contains(v)) throw false;});
return true;
}catch(x){
if(x===false) return false; // 如果集合中有元素在另外一个集合中不存在
throw x; // 重新抛出异常
}
}
如果将对象用于Javascript的关系比较运算符,比如:<和<=,Javascript会首先调用对象的valueOf方法,如果这个方法返回一个原始值,则直接比较原始值;但大多数类并没有valueOf()方法,为了按照显式定义的规则来比较这些类型的对象,可以定义一个compareTo()的方法;如:
js
Range.prototype.compareTo = function(that){
if(!(that instanceof Range))
throw new Error("Can't compare a Range with " + that);
var diff = this.from - that.from; // 比较下边界
if(diff == 0) diff = this.to - that.to; // 如果相等,再比较上边界
return diff;
}
给类定义了compareTo()方法后,可以对类的实例组成的数组进行排序了;Array.sort()方法可以接收一个可选的参数,这个参数是一个函数,用来比较两个值的大小,这个函数返回值的约定和compareTo()方法保持一致,如:
js
rangs.sort(function(a,b){return a.compareTo(b);});
排序运算非常重要,如果已经为类定义了实例方法compareto(),还应当参照这个方法定义一个可传入这两个参数的比较函数;如:
js
Range.byLowerBound = function(a,b){
return a.compareTo(b);
};
ranges.sort(Range.byLowerBound);
18-4 方法借用
多个类中的方法可以共用同一个单独的函数,比如,Array类通常定义了一些内置方法,如果定义了一个类,它的实例是类数组的对象,则可以从Array.prototype中将函数复制至所定义的类的原型对象中;
如果以经典的面向对象语言的视角来看Javascript的话,把一个类的方法用到其他的类中的做法也称做:多重继承,然而,Javascript并不是经典的面向对象语言,所以将这种方法重用称为"方法借用";
不仅Array的方法可以借用,还可以自定义泛型方法,如:
js
var generic = {
// 返回一个字符串,这个字符串包含构造函数的名字
// 以及所有非继承来的、非函数属性的名字和值
toString: function(){
var s = '[';
if(this.constructor && this.constructor.name)
s += this.constructor.name + ": ";
// 枚举所有非继承且非函数的属性
var n = 0;
for(var name in this){
if(!this.hasOwnProperty(name)) continue; // 跳过继承的属性
var value = this[name];
if(typeof value === "function") continue // 跳过方法
if(n++) s += ", ";
s += name + "=" + value;
}
return s + ']';
},
// 这种方法适合于那些实例属性是原始值的情况
// 这里还处理一种特殊的情况,就是忽略由Set类添加的特殊属性
equals: function(that){
if(that == null) return false;
if(this.constructor !== that.constructor) return false;
for(var name in this){
if(name === "|**objectid**|") continue; // 跳过特殊属性
if(!this.hasOwnProperty(name)) continue; // 跳过继承来的属性
if(this[name] !== that[name]) return false; // 比较是否相等
}
return true; // 如果所有属性都匹配,两个对象相等
}
};
Range.prototype.equals = generic.equals;
18-5 私有状态
在经典的面向对象编程中,经常需要将对象的某个状态封装或隐藏在对象内,只有通过对象的方法才能访问这些状态,对外只暴露一些重要的状态变量可以直接读写;为了实现这个目的,类似Java的编程语言允许声明类的"私有"实例字段,这些私有实例字段只能被类的实例方法访问,且在类的外部是不可见的。
可以通过将变量(或参数)闭包在一个构造函数内来模拟实现私有实例字段,调用构造函数会创建一个实例;
js
// 对Range类的读取端点方法的简单封装
function Range(from, to){
// 不要将端点保存为对象的属性,相反定义存取器函数来返回端点的值
// 这些值都保存在闭包中
this.from = function(){return from;};
this.to = function(){return to;}
}
// 原型上的方法无法直接操作端点,它们必须调用存取器方法
Range.prototype = {
constructor: Range,
includes: function(x){return this.from() <=x && x <= this.to();},
foreach: function(f){
for(var x = Math.ceil(this.from()), max = this.to(); x <= max; x++)
f(x);
},
toString:function(){return "(" + this.from() + "..." + this.to() + ")";}
}
这种封装技术造成了更多系统开销,使用闭包来封装类的状态的类一定会比不使用封装的状态变量的等价类运行速度更慢,并占用更多内存。
18-6 构造函数的重载和工厂方法
有时候,对象的初始化需要多种方式,可以通过重载这个构造函数让它根据传入参数的不同来执行不同的初始化方法,如,重载Set构造函数:
js
function Set(){
this.values = {};
this.n = 0;
// 如果传入一个类数组的对象,将这个元素添至集合中
// 否则,将所有的参数都添加至集合中
if(arguments.length == 1 && isArrayLike(arguments[0]))
this.add.apply(this, arguments[0]);
else if(arguments.length > 0)
this.add.apply(this,arguments);
}
通过工厂方法使用数组初始化Set对象:
js
Set.fromArray = function(a){
s = new Set(); // 创建一个空集合
s.add.apply(s,a); // 将数组a的成员作为参数传入add()方法
return s;
}
在ES中是可以定义多个构造函数继承自一个原型对象的,由这些构造函数的任意一个所创建的对象都属于同一类型;:
js
// Set类的一个辅助构造函数
function SetFromArray(a){
// 通过以函数的形式调用Set()来初始化这个新对象
// 将a的元素作为参数传入
Set.apply(this, a);
}
// 设置原型,以便SetFromArray能创建Set的实例
SetFromArray.prototype = Set.prototype;
var s = new SetFromArray([1,2,3]);
console.log(s instanceof Set); // true
19,继承
19-1 继承机制的实现
- 要实现继承,首先定义父类(基类);基于父类,再定义子类;
- 有些父类不能直接使用,只是为了让子类继承,此类为抽象类;
- 创建的子类将继承父类的所有属性和方法,包括构造函数及方法的实现(不包括私有成员);
- 子类可以添加父类中没有的新属性和方法,也可以覆盖父类中的属性和方法;
19-2 构造函数继承
在JS中实现继承的方式不止一种,因为JS中的继承机制并不是明确规定的,而是通过模仿实现的;
借用构造函数(对象冒充):构造函数使用this关键字给所有属性和方法赋值,因为构造函数只是一个函数,所以可使classA的构造函数成为classB的方法,然后调用它classB就会得到classA的构造函数中定义的属性和方法:
js
function ClassA(sColor){
this.color=sColor;
this.sayColor=function(){
console.log(this.color);
}
}
function ClassB(sColor){
this.newMethod=ClassA;
this.newMethod(sColor);
delete this.newMethod;
}
子类还可以添加自己的成员:
js
function ClassA(sColor){
this.color=sColor;
this.sayColor=function(){
console.log(this.color);
}
}
function ClassB(sColor,sName){
this.newMethod=ClassA;
this.newMethod(sColor);
delete this.newMethod;
this.name=sName;
this.sayName=function(){
console.log(this.name);}
}
var objA=new ClassA("red");
var objB=new ClassB("blue","wangwei");
objA.sayColor();
objB.sayColor();
objB.sayName();
多继承:一个类可以继承多个父类;
js
function ClassX(){}
function ClassY(){}
function ClassZ(){
this.newMethod=ClassX;
this.newMethod();
delete this.newMethod;
this.newMethod=ClassY;
this.newMethod();
delete this.newMethod;
}
以上的实现继承的方法很流行,因此JS专门为Function对象加入了两个新方法,即call()和apply() 用于继承的实现;
此种方法与传统的方法一样,只不过把新方法的赋值、调用和删除换成call方法或apply()即可;如:
js
function ParentType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){
ParentType.call(this);
}
var s1 = new SubType();
s1.colors.push("black");
console.log(s1.colors);
var s2 = new SubType();
console.log(s2.colors);
// 带参数
function ClassB(sColor){//}
function ClassB(sColor,sName){
ClassA.call(this,sColor);
this.name=sName;
this.sayName=function(){
console.log(this.name);
}}
//...
ParentType.sayCors = function(){
console.log("show colors");
}
//...
s1.sayCors(); // s1.sayCors is not a function
19-3 原型继承
原型继承并没有使用严格意义上的构造函数,而是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型;如:
js
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function object(o){
function F(){}
F.prototype = o;
return new F();
}
var person = {
name:"wangwei",
friends:["wujing","lishi"]
};
var p1 = object(person);
p1.name = "wujing";
p1.friends.push("guanli");
var p2 = object(person);
p2.name = "toto";
p2.friends.push("adu");
console.log(p1.friends); // wujing,lishi,guanli,adu
console.log(person.friends); // wujing,lishi,guanli,adu
object()完全可以由Object.create()代替,如:
js
var p1 = Object.create(person);
19-4 原型链
js
// ES对象的属性有两种,一种是自有属性(own property),另一种是从原型对象继承而来的;
// ES将原型链作为实现继承的主要方法;
// 基本思想是利用原型让一个对象继承另一个对象的属性和方法;
// 原型链的基本概念原理:构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针;此时,可以让原型对象等于另一个类型的实例,即该原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针;如果另一个原型又是另一个类型的实例,以上关系依然成立,如此层层递进,就构成了实例与原型的链条;这就是所谓的原型链的基本概念。
实现原型链有一种基本模式,如:
js
function SuperType(){
this.supproperty = true;
}
SuperType.prototype.getSuperValue = function(){
return this.supproperty;
};
function SubType(){
this.subproperty = true;
}
SubType.prototype = new SuperType(); // 继承了SuperType
SubType.prototype.getSubTypeValue = function(){
return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); // true
通过原型链,本质上扩展了之前所说的原型搜索机制;即,当以读取模式访问一个实例属性时,首先会在实例中搜索该属性,如果没有找到,则会继续搜索实例的原型;在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。
19-4-1 默认的原型:
所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的;当调用instance.toString()时,实际上调用的是保存在Object.prototype中的那个方法。
19-4-2 确定原型和实例的关系:
可以通过两种方式来确定原型和实例之间的关系;第一种方式使用instanceof操作符,测试实例与原型链中出现过的构造函数,均返回true;如:
js
alert(instance instanceof Object); // true
alert(instance instanceof SuperType); // true
alert(instance instanceof SubType);
第二种为isPrototypeOf方法,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,均返回true,如:
js
alert(Object.prototype.isPrototypeOf(instance)); // true
alert(SuperType.prototype.isPrototypeOf(instance)); // true
alert(SubType.prototype.isPrototypeOf(instance)); // true
19-4-3 谨慎地定义方法:
子类型有时候需要覆盖超类型中的某个方法,或者需要添加超类中不存在的某个方法;但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后,如
js
// 接前例
SubType.prototype = new SuperType(); // 继承了SuperType
SubType.prototype.getSubTypeValue = function(){
return this.subproperty;
};
SubType.prototype.getSuperValue = function(){ // 重写父类的方法
return false;
}
var instance = new SubType();
alert(instance.getSuperValue());
通过原型链实现继承时,谨慎使用对象字面量添加原型方法,因为其本质是会重写原型链;如:
js
// 在前面的示例中改写
SubType.prototype = new SuperType(); // 继承了SuperType
SubType.prototype = { // 使用字面量添加新方法,会导致上一行代码无效,但注释这一段后就没有问题
getSubValue:function(){
return this.subproperty;
},
otherMethod:function(){
return false;
}
};
var instance = new SubType();
alert(instance.getSuperValue()); // error;
19-4-4 原型链的问题
原型链虽然很强大,可以用它来实现继承,但它也存在一些问题;其中,最主要的问题来自包含引用类型值的原型,因为原型链中的包含引用类型值的原型属性会被所有实例共享,这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因;如:
js
function SuperType(){
this.colors = ["red","blue","green"];
}
function SubType(){}
SubType.prototype = new SuperType(); // 继承了SuperType
var instance = new SubType();
instance.colors.push("black");
console.log(instance.colors); // red,blue,green,black
var instance2 = new SubType();
console.log(instance2.colors); // red,blue,green,black
第二个问题,在创建子类的实例时,不能向超类的构造函数中传递参数; 即没有办法在不影响所有对象实例的情况下,给超类的构造函数传递参数;
19-5 组合继承
组合继承(combination inheritance),有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式;原理是:用借用构造函数来实现对实例的属性的继承,用原型链继承prototype对象的属性和方法,这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性,如:
js
function Super(name){
this.name = name;
this.colors = ["red","blue","green"];
}
Super.prototype.sayName = function(){
console.log(this.name);
};
function Suber(name,age){
Super.call(this,name); // 继承了SuperType
this.age = age;
}
Suber.prototype = new Super(); // 继承属性
Suber.prototype.constructor = Suber;
Suber.prototype.sayAge = function(){
console.log(this.age);
}
var instance = new Suber("wangwei",18);
instance.colors.push("black");
console.log(instance.colors); // red,blue,green,black
instance.sayName(); // wangwei
instance.sayAge(); // 18
var instance2 = new Suber("wujing",27);
console.log(instance2.colors); // red,blue,green
instance2.sayName(); // wujing
instance2.sayAge(); // 27
19-6 寄生式继承
寄生式(parasitic)继承同样是道格拉斯.克罗克福提出,并与原型继承相关;其思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再返回对象;如:
js
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function createAnother(original){
var clone = object(original); // 通过调用函数创建一个新对象
// var clone = Object.create(original);
clone.sayHi = function(){ // 以某种方式增强这个对象
console.log("Hi");
};
return clone;
}
// 使用createAnother()函数
var person = {
name:"wangwei",
friends:["wujing","lishi"]
};
var p1 = createAnother(person);
p1.sayHi(); // Hi
19-7 寄生组合
前文所说的组合继承是ES最常用的继承模式,不过,它也有不足;组合式最大的不足是调用了两次超类的构造函数:一次是在创建子类型原型时、另一次是在子类构造函数内部;如:
js
function Super(name){
this.name = name;
this.colors = ["red","green","blue"];
}
Super.prototype.sayName = function(){
console.log(this.name);
};
function Suber(name,age){
Super.call(this,name); // 第二次调用SuperType
this.age = age;
}
Suber.prototype = new Super(); // 第一次调用SuperType()
Suber.prototype.constructor = Suber;
Suber.prototype.sayAge = function(){
console.log(this.age);
}
var s = new Suber("wangwei",18);
console.log(s);
console.log(Object.getPrototypeOf(s));
console.log(s.hasOwnProperty("name")); // true
console.log(s.hasOwnProperty("colors")); // true
可以采用寄生组合式继承解决这个问题;
寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法;
基本思路:不必为了指定子类的原型而调用超类的构造函数,所需要的无非就是超类原型的一个副本;本质上就是使用寄生式继承超类的原型,再将结果指定给子类的原型;
寄生组合式继承的基本模式如:
js
function inheritPrototype(subType, superType){
var prototype = Object.create(superType.prototype); // 创建对象
prototype.constructor = subType; // 指定构造函数为subType
subType.prototype = prototype; // 指定subType原型为prototype
}
function SubType(name,age){
SuperType.call(this,name);
this.age = age;
}
inheritPrototype(SubType, SuperType); // 替换了原来的两行
SubType.prototype.sayAge = function(){
console.log(this.age);
}
19-8 示例(形状)
js
一.创建基类: Polygon([ˈpɒlɪɡən] area:[ˈeəriə]):
function Polygon(iSides){
this.sides=iSides;
}
Polygon.prototype.getArea=function(){
return 0;
}
创建子类 Triangle: 确定为3条边,所以覆盖Polygon类的sides属性,设置为3;使用三角形的面积公式:1/2底高来覆盖getArea()方法,因此需要新属性 base 和 height ;
js
function Triangle(iBase,iHeight){
Polygon.call(this,3);
this.base=iBase;
this.height=iHeight;
}
Triangle.prototype=new Polygon();
Triangle.prototype.getArea=function(){
return 0.5*this.base*this.height;
}
创建子类 Rectangle:确定为4条边,所以覆盖Polygon类的sides属性,设置为4;使用矩面积公式:长*宽来覆盖getArea()方法,需要新属性 length 和 width;
js
function Rectangle(iLength,iWidth){
Polygon.call(this,4);
this.length=iLength;
this.width=iWidth;
}
Rectangle.prototype=new Polygon();
Rectangle.prototype.getArea=function(){
return this.length*this.width;
}
var triangle=new Triangle(12,4);
var rectangle=new Rectangle(20,10);
console.log(triangle.sides);
console.log(triangle.getArea());
console.log(rectangle.sides);
console.log(rectangle.getArea());
19-9 深入继承与应用
扩展应用,封装一个函数:
JS
function defineSubClass(superclass, // 父类的构造函数
constructor, // 子类的构造函数
methods, // 实例方法:复制至原型中
statics) // 类属性:复制到构造函数中
{
// 建立子类的原型对象
constructor.prototype = Object.create(superclass.prototype);
constructor.prototype.constructor = constructor;
// 像对常规类一样复制方法和类属性
if(methods) extend(constructor.prototype, methods);
if(statics) extend(constructor, statics);
return constructor;
}
// 可以添加到Function的原型中
Function.prototype.extend = function(constructor,methods,statics){
return defineSubClass(this, constructor,methods,statics);
};
定义一个Set类的子类SingletonSet,此类是一个特殊的集合,它是只读的,而且含有单独的常量成员:
js
// SingletonSet构造函数
function SingletonSet(member){
this.member = member; // 集合中唯一的成员
}
// 创建一个原型对象,这个原型对象继承自Set的原型
SingletonSet.prototype = Object.create(Set.prototype);
// 给原型添加属性,如果有同名的就覆盖Set.prototype中同名属性
extend(SingletonSet.prototype, {
constructor: SingletonSet,
// 这个集合是只读的,调用add()或remove()都会报错
add: function(){throw "read-only set";},
remove: function(){ throw "read-only set";},
// SingletonSet的实例中永远只有一个元素
size: function(){return 1;},
// 这个方法只调用一次,传入这个集合的唯一成员
foreach: function(f,context){f.call(context, this.member)},
// 检查传入的值是否匹配这个集合唯一的成员
contains: function(x){return x === this.member;}
});
也可以为SingletonSet类定义自己的equals(),会更高效一些,如:
js
SingletonSet.prototype.equals = function(that){
return that instanceof Set && that.size() == 1 && that.contains(this.member);
};
利用构造函数和原型链的示例,比如定义了Set类的子类NonNullSet,它不允许null和undefined作为它的成员;为了使用这种方式对成员做限制,NonNullSet需要在其add()方法中对null和undefined值做检测;但是,它需要完全重新实现一个add()方法:
js
// NonNullSet类是Set的子类,它的成员不能是null或undefined
function NonNullSet(){
// 仅链接到父类
// 作为普通函数调用父类的构造函数来初始化通过该构造函数调用创建的对象
Set.apply(this, arguments);
}
// 将NonNullSet设置为Set的子类
NonNullSet.prototype = Object.create(Set.prototype);
NonNullSet.prototype.constructor = NonNullSet;
// 为了将null和undefined排除在外,只须重写add()方法
NonNullSet.prototype.add = function(){
// 检查参数是不是null或undefined
for(var i=0; i<arguments.length; i++){
if(arguments[i] == null)
throw new Error("Can't add null or undefined to a NonNullSet");
}
// 调用父类的add()方法以执行实际插入操作
return Set.prototype.add.apply(this.arguments);
};
NonNullSet类只是在执行add()时,对参数进行了过滤;为此,可以定义一个类工厂函数,传入一个过滤函数,返回一个新的Set子类;:
js
// 类工厂和方法链
// 这个函数返回具体Set类的子类
// 关重写该类的add()方法用以对添加的元素做特殊处理
function filterSetSubClass(superclass, filter){
// 子类构造函数
var constructor = function(){
superclass.apply(this, arguments); // 调用父类构造函数
};
var proto = constructor.prototype = Object.create(superclass.prototype);
proto.constructor = constructor;
proto.add = function(){
// 在添加任何成员之前首先使用过滤器将所有参数进行过滤
for(var i=0; i<arguments.length; i++){
var v = arguments[i];
if(!filter(v)) throw("value " + v + " rejected by filter");
}
// 调用父类的add()方法
superclass.prototype.add.apply(this, arguments);
};
return constructor;
}
// 定义一个只能保存字符串的集合类
var StringSet = filterSetSubClass(Set, function(x){return typeof x === "string";});
// 定义一个成员不能是null或undefined或函数
var MySet = filterSetSubClass(Set, function(x){return typeof x !== "function";});
类似这种创建类工厂的能力是ES语言动态特性的一个体现,类工厂是一种非常强大和有用的特性,比如,可以使用Function.prototype.extend()方法重写NonNullSet:
js
var NonNullSet = (function(){
var superclass = Set; // 指定父类
return superclass.extends(
function(){superclass.apply(this, arguments)}, // 构造函数
{
add: function(){
for(var i=0; i<arguments.length; i++){
if(arguments[i] == null)
throw new Error("Can't add null or undefined");
}
// 调用父类的add()方法以执行实际插入操作
return superclass.prototype.add.apply(this,arguments);
}
});
}());
使用组合代替继承的集合的实现:
js
// 实现一个FilterSet,它包装某个指定的"集合"对象
// 并对传入add()方法的值应用了某种指定的过滤器
// "范围"类中其他所有的核心方法延续到包装后的实例中
var FilterSet = Set.extend(
// 构造函数
function FilterSet(set, filter){
this.set = set;
this.filter = filter;
},
{ // 实例方法
add: function(){
// 如果已有过滤器,直接使用它
if(this.filter){
for(var i=0; i<arguments.length; i++){
var v = arguments[i];
if(!this.filter(v))
throw new Error("FilterSet: value " + v + " rejected by filter");
}
}
// 调用set中的add
this.set.add.apply(this.set, arguments);
return this;
},
// 剩下的方法保持不变
remove: function(){
this.set.remove.apply(this.set, arguments);
},
contains: function(v){return this.set.contains(v);},
size: function(){return this.set.size()},
foreach: function(f,c){this.set.foreach(f,c);}
});
可以利用这个类的实例来创建任意带有成员限制的集合实例:
js
var s = new FilterSet(new Set(), function(x){return x !== null;});
// 还可以对已经过滤后的集合进行再过滤
var t = new FilterSet(s,function(x){return !(x instanceof Set);});
19-10 抽象类
抽象类用来描述一种类型应该具备的基本特征与功能, 具体如何去完成这些行为由子类通过方法重写来完成;即抽象方法指只有功能声明,没有功能主体实现的方法;具有抽象方法的类一定为抽象类。
抽象类不能实例化对象,但类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样;所以只能被子类继承后,创建子类对象;子类需要继承抽象父类并完成最终的方法实现细节(即重写方法,完成方法体);而此时,方法重写不再是加强父类方法功能,而是父类没有具体实现,子类完成了具体实现,将这种方法重写也叫做实现方法。
由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用;
js
// 此函数可以做任何抽象方法
function abstractmethod(){throw new Error("abstract method;");}
// 定义了AbstractSet类,并定义抽象方法contains()
function AbstractSet(){throw new Error("Can't instantiate abstract classes");}
AbstractSet.prototype.contains = abstractmethod;
// NotSet是AbstractSet的一个非抽象子类
// 所有不在其他集合中的成员都在这个集合中
// 因为它是在其他集合中是不可写的条件下定义的
// 同时由于它的成员是无限个,因此它是不可枚举的
// 只能用它来检测元素成员的归属情况
// 使用了Function.prototype.extends()方法定义的
var NotSet = AbstractSet.extend(
function NotSet(set) {this.set = set;},
{
contains: function(x) {return !this.set.contains(x);},
toString: function(x){return "~" + this.set.toString();},
equals: function(that){return that instanceof NotSet && this.set.equals(that.set);}
}
);
// AbstractEnumerableSet是AbstractSet的一个抽象子类
// 它定义了抽象方法size()和foreach()
// 然后实现了非抽象方法isEmpty()、toArray()、to[Locale]String()和equals()
// 子类实现了contains()、size()和foreach,这三个方可以很轻易的调用这5个非抽象方法
var AbstractEnumerableSet = AbstractSet.extend(
function(){throw new Error("Can't instatiate abstract classes");},
{
size: abstractmethod,
foreach: abstractmethod,
isEmpty: function(){return this.size() == 0;},
toString: function(){
var s = "{", i=0;
this.foreach(function(v){
if(i++>0) s += ", ";
s += v;
});
return s + "}";
},
toLocalString: function(){
var s = "{", i=0;
this.foreach(function(v){
if(i++>0) s += ", ";
if(v == null) s += v; // null和undefined
else s += v.toLocalString(); // 其他的情况
});
return s + "}";
},
toArray: function(){
var a = [];
this.foreach(function(v){ a.push(v);});
return a;
},
equals: function(that){
if(!(that instanceof AbstractEnumerableSet)) return false;
// 如果它们的大小不同,则它们不相等
if(this.size() != that.size()) return false;
// 检查每一个元素是否也在that中
try{
this.foreach(function(v){if(!that.contains(v)) throw false;});
return true; // 所有的元素都匹配:集合相等
}catch(x){
if(x === false) return false; // 集合不相等
throw x; // 发生了其他的异常:重新抛出异常
}
}
});
// SingletonSet是AbstractEnumerableSet的非抽象子类
// SingletonSet集合是只读的,它只包含一个成员
var SingletonSet = AbstractEnumerableSet.extend(
function SingletonSet(member){this.member = member;},
{
contains: function(x){return x === this.member;},
size: function(){return 1;},
foreach: function(f,ctx){f.call(ctx, this.member);}
}
);
// AbstractWritableSet是AbstractEnumerableSet的抽象子类
// 它定义了抽象方法add()和remove()
// 然后实现了非抽象方法union()、intersection()和difference()
var AbstractWritableSet = AbstractEnumerableSet.extend(
function(){throw new Error("Can't instatiate abstract classes");},
{
add: abstractmethod,
remove: abstractmethod,
union: function(that){
var self = this;
that.foreach(function(v){self.add(v);});
return this;
},
intersection: function(that){
var self = this;
this.foreach(function(v){if(!that.contains(v)) self.remove(v);});
return this;
},
difference: function(that){
var self = this;
that.foreach(function(v){self.remove(v);});
return this;
}
});
// ArraySet是AbstractWritableSet的非抽象子类
// 它以数组的形式表示集合中的元素
// 对于它的contains()方法使用了数组的线性查找
// 因为contains()方法的算法复杂度是0(n)而不是0(1)
// 它非常适用于相对小型的集合
var ArraySet = AbstractWritableSet.extend(
function ArraySet(){
this.values = [];
this.add.apply(this, arguments);
},
{
contains:function(v){return this.values.indexOf(v) != -1;},
size: function(){return this.values.length;},
foreach: function(f,c){this.values.forEach(f,c);},
add: function(){
for(var i=0;i<arguments.length; i++){
var arg = arguments[i];
if(!this.contains(arg)) this.values.push(arg);
}
return this;
},
remove: function(){
for(var i=0;i<arguments.length; i++){
var p = this.values.indexOf(arguments[i]);
if(p == -1) continue;
this.values.splice(p,1);
}
return this;
}
});
function StringSet(){
this.set = Object.create(null); // 创建一个不包含原型的对象
this.n = 0;
this.add.apply(this, arguments);
}
// 在此指定了属性的特性
StringSet.prototype = Object.create(AbstractWritableSet.prototype, {
constructor: {value: StringSet},
contains: {value: function(x){return x in this.set;}},
size: {value: function(x){return this.n;}},
foreach: {value: function(f,c){ Object.keys(this.set).forEach(f,c);}},
add: {
value: function(){
for(var i=0; i<arguments.length; i++){
if(!(arguments[i] in this.set)){
this.set[arguments[i]] = true;
this.n++;
}
}
return this;
}
},
remove: {
value: function(){
for(var i=0; i<arguments.length; i++){
if(arguments[i] in this.set){
delete this.set[arguments[i]];
this.n--;
}
}
return this;
}
}
});
可以为集合中某个成员添加一些类似于"对象id"属性,默认是可以通过for/in遍历的,但也可以让属性不可枚举:
js
// 定义不可枚举的属性, 封装在一个匿名函数中
(function(){
// 定义一个不可枚举的属性objectId,它可以被所有对象继承
// 定义了getter,没定义setter,不可配置的
Object.defineProperty(Object.prototype, "objectId", {
get: idGetter, // 取值器
enumerable: false,
configurable: false
});
// 当读取objectId时调用这个getter函数
function idGetter(){
if(!(idprop in this)){ // 如果对象不存在id
if(!Object.isExtensible(this)) // 如果不可扩展,即不能添加属性
throw Error("Can't define id for noextensible objects");
Object.defineProperty(this, idprop, {
value: nextid++,
writable: false,
enumerable: false,
configurable: false
});
}
return this[idprop];
}
// idGetter用到这些变量,这些都属于私有变量
var idprop = "|**objectId**|";
var nextid = 1; // 给它设置初始值
}());
对象的属性可设置为只读的,同样,实例也可以定义不可变的:
js
// Range可以使用new,也可省略new,即可以用做构造函数也可以用作工厂函数
function Range(from,to){
// 这些是对from和to只读属性的描述符
var props = {
from: {value: from, enumerable:true,writable:false,configurable:false},
to:{value:to, enumerable:true,writable:false,configurable:false}
};
if(this instanceof Range)
Object.defineProperties(this, props);
else
return Object.create(Range.prototype, props);
}
// 用同样的方法给Range.prototype对象添加属性(符)
Object.defineProperties(Range.prototype, {
includes:{value: function(x){return this.from <= x && x <= this.to;}},
foreach:{value: function(f){
for(var x = Math.ceil(this.from); x <= this.to; x++) f(x);
}},
toString:{
value: function(){return "(" + this.from + "..." + this.to + ")";}
}
});
改进的做法:将修改这个已定义属性的特性的操作定义为一个工具函数,如:
js
// 属性描述符工具函数
// 将o的指定名字(或所有)的属性设置为不可写的和不可配置的
function freezeProps(o){
var props = (arguments.length == 1) // 如果只有一个参数
? Object.getOwnPropertyNames(o) // 使用所有的属性
: Array.prototype.splice.call(arguments, 1); // 否则传入了指定名字的属性
props.forEach(function(n){ // 将它们设置为只读和不可变的
// 忽略不可配置的属性
if(!Object.getOwnPropertyDescriptor(o,n).configurable) return;
Object.defineProperty(o,n,{writable:false, configurable:false});
});
return o;
}
// 将o的指定名字(或所有)的属性设置为不可枚举的和可配置的
function hideProps(o){
var props = (arguments.length == 1) // 如果只有一个参数
? Object.getOwnPropertyNames(o)
: Array.prototype.splice.call(arguments, 1);
props.forEach(function(n){
if(!Object.getOwnPropertyDescriptor(o,n).configurable) return;
Object.defineProperty(o,n,{enumerable: false});
});
return o;
}
// 应用
function Range(from,to){
this.from = from;
this.to = to;
freezeProps(this); // 将属性设置为不可变的
}
Range.prototype = hideProps({
constructor: Range,
includes: function(x){return this.from <=x && x <= this.to;},
foreach: function(f){for(var x=Math.ceil(this.from); x<=this.to; x++) f(x);},
toString: function(){return "(" + this.from + "..." + this.to + ")";}
});
可以通过定义属性getter和setter方法将状态变量更健壮地封装起来,如:
js
function Range(from,to){
if(this.from > to) throw new Error("Range: from must be <= to");
// 定义存取器方法以维持不变
function getFrom(){return from;}
function getTo(){return to;}
function setFrom(f){ // 设置from的值时,不允许from大于to
if(f <= to) from = f;
else throw new Error("Range: from must be <= to");
}
function setTo(t){
if(t >= from) to = t;
else throw new Error("Range: to must be >= from");
}
// 将使用取值器的属性设置为可枚举的,不可配置的
Object.defineProperties(this, {
from:{
get: getFrom,
set: setFrom,
enumerable: true, configurable:false
},
to:{
get:getTo,
set:setTo,
enumerable: true, configurable: false
}
});
}
Range.prototype = hideProps({
constructor: Range,
includes: function(x){return this.from <=x && x <= this.to;},
foreach: function(f){for(var x=Math.ceil(this.from); x<=this.to; x++) f(x);},
toString: function(){return "(" + this.from + "..." + this.to + ")";}
});
19-11 防止类的扩展
js
Object.freeze(enumeration.values);
Object.freeze(enumeration);
19-12 原型中的属性描述符
js
// 如果不带参数调用它,就表示该对象的所有属性
// 将所有逻辑闭包在一个私有属性作用域中
(function namespace(){
// properties构造函数,其表示一个对象的属性集合
function Properties(o, names){
this.o = o; // 属性所属的对象
this.names = names; // 属性的名字,是一个数组
}
// 这个函数成为所有对象的方法
function properties(){
var names; // 属性名组成的数组
if(arguments.length == 0) // 所有有自有属性
names = Object.getOwnPropertyNames(this);
else if(arguments.length == 1 && Array.isArray(arguments[0]))
names = arguments[0]; // 参数本身就是一个数组
else // 参数本身就是名字
names = Array.prototype.splice.call(arguments,0);
// 返回一个新的Properties对象,用以表示属性名字
return new properties(this, names);
}
// 将properties设置为Object.prototype的新的不可枚举的属性(方法)
// 这是从私有函数作用域导出的唯一的一个值
Object.defineProperty(Object.prototype, "properties", {
value: properties,
enumerable: false,writable:true,configurable:true
});
// 将代表这些属性的对象设置为不可枚举的
Properties.prototype.hide = function(){
var o = this.o;
var hidden = {enumerable: false};
this.names.forEach(function(n){
if(o.hasOwnProperty(n))
Object.defineProperty(o, n, hidden);
});
return this;
};
// 将这些属性设置为只读的和不可配置的
Properties.prototype.freeze = function(){
var o = this.o;
var frozen = {writable: false, configurable: false};
this.names.forEach(function(n){
if(o.hasOwnProperty(n))
Object.defineProperty(o,n,frozen);
});
return this;
};
// 返回一个对象,这个对象是名字到属性描述符的映射表
// 使用它来复制属性,连同属性特性一起复制
// Object.defineProperties(dest, src.properties().descriptors());
Properties.prototype.descriptors = function(){
var o = this.o;
var desc = {};
this.names.forEach(function(n){
if(!o.hasOwnProperty(n)) return;
desc[n] = Object.getOwnPropertyDescriptor(o,n);
});
return desc;
};
// 返回一个格式化良好的属性列表
// 列表中包含名字、值和属性特性,使用permanent表示不可配置
// 使用readonly表示不可写,使用hidden表示不可枚举
// 普通的可枚举、可写和可配置属性不包含特性列表
Properties.prototype.toString = function(){
var o = this.o; // 在下面嵌套的函数中使用
var lines = this.names.map(nameToString);
return "{\n " + lines.joing(",\n ") + "\n}";
function nameToString(n){
var s = "";
var desc = Object.getOwnPropertyDescriptor(o, n);
if(!desc) return "no exist " + n + ": undefined";
if(!desc.configurable)
s += "Permanent ";
if((desc.get && !desc.set) || !desc.writable)
s += "readonly ";
if(!desc.enumerable)
s += "hidden ";
if(desc.get || desc.set)
s += "accessor " + n;
else
s += n + ": " + ((typeof desc.value === "function") ? "function" : desc.value);
return s;
}
};
// 最后,将原型对象中的实例方法设置为不可枚举的
Properties.prototype.properties().hide();
}());
20,Web浏览器中的Javascript
20-1 Web浏览器中的Javascript
通常也称为客户端的JavaScript,就是JavaScript运行在浏览器中;
从内容上来看,它是包括BOM和DOM;
从形式上可以分为Web文档和Web应用两种形式;
20-2 Web文档里的Javascript
Javascript可以通过document对象和它包含的element对象遍历和管理文档内容;它可以通过操纵CSS样式和类,修改文档内容的呈现;并且可以通过注册适当的事件处理程序来定义文档元素的行为;
Web文档里应当少量地应用Javascript,因为Javascript真正的作用是增强用户的浏览体验,使信息的获取和传递更容易;用户的体验不应依赖于Javascript,但Javascript可以增强体验,如:
- 创建动画和其他视觉效果,引导和帮助用户进行页面导航;
- 对表格的列进行分组,让用户更容易找到所需要的内容;
- 隐藏某些内容,当用户需要了解更详细内容时,再逐渐展示详细信息;
20-3 window对象
客户端Javascript中最重要的对象之一是window对象,window对象是所有客户端Javascript特性和API的主要接入点;它表示Web浏览器的一个窗口或窗体;Window对象定义了一些属性,比如,指定当前窗口中的URL的location属性,其还可以允许脚本在窗口中载入新的URL;
js
window.location = "https://www.zeronetwork.cn/";
window对象还定义了一些方法,如alert(),可以弹出一个对话框用来显示一些信息;比如:setTimeout(),可以注册一个函数,在给定的一段时间之后触发一个回调,如:
js
setTimeout(function(){
alert("零点网络");
},2000);
在客户端Javascript中,window对象也是全局对象,也就是window对象处于作用域链的最顶部,它的属性和方法实际上是全局变量和全局函数,所以,window.setTimeout()可以直接使用setTimeout(),也就是说,如果想引用全局窗口或全局对象的属性,通常并不需要用到window;
window还定义了很多其它重要的属性、方法和构造函数;其中最重要的属性是document,它引用Document对象,表示的是在窗口中的文档;Document对象有一些重要的方法,比如getElementById(),可以基于元素id的值返回单一的HTML元素,如:
js
var mydiv = document.getElementById("mydiv");
getElementById()方法返回的Element对象,也拥有一些重要的属性和方法,比如允许脚本获取它的内容、设置属性值等,如:
var mydiv = document.getElementById("mydiv");
// 如果元素为空,则往里面插入当前的日期和时间
if(mydiv.firstChild == null){
mydiv.appendChild(document.createTextNode(new Date().toString()));
}
每个Element对象才有style和className属性,允许脚本指定文档元素的CSS样式,或修改应用到元素上的CSS的类名,如:
js
var mydiv = document.getElementById("mydiv");
mydiv.style.height = "200px";
mydiv.style.backgroundColor = "yellow";
mydiv.className = "mydiv";
window对象、document对象和element对象还有一个重要的属性集合是事件处理程序相关的属性;可以在脚本中为之绑定一个函数,这个函数会在某个事件发生时以异步的方式调用;事件处理程序可以让Javascript代码修改窗口、文档和组成文档的元素的行为;事件处理程序的属性名是以单词"on"开头的,如:
js
mydiv.onclick = function(){
this.innerHTML = "<h2>零点网络</h2>";
}
window对象的onload处理程序是最重要的事件处理程序之一;当显示在窗口中的文档内容稳定并可以操作时可以触发它;Javascript代码通常封装在onload事件处理程序里;比如,可以在onload事件中,查询文档元素、修改CSS类和定义事件处理程序,如:
js
<style>
.newslist *{display: none;}
.newslist h1{display: block;}
.newslist_show *{display: block;}
</style>
<script>
window.onload = function(){
var elements = document.getElementsByClassName("newslist");
for(var i=0; i<elements.length; i++){
var elt = elements[i];
var title = elt.getElementsByTagName("h1")[0]
showHandler(title,elt);
}
function showHandler(title,elt){
title.onclick = function(){
if(elt.className == "newslist")
elt.className = "newslist_show";
else
elt.className = "newslist";
}
}
}
</script>
<div class="newslist">
<h1>零点网络</h1>
<p>零点网络是一家科技公司</p>
</div>
20-4 Web应用里的Javascript
在Web文档中使用的Javascript特性在Web应用中都会用到,对于Web应用来说,除了内容、呈现和操作API之外,还依赖Web浏览器环境提供的各种基础的服务;
现代浏览器,已经不仅仅是作为显示文档的工具了,而渐渐变成了一个简易的操作系统;
Web应用就是用Javascript访问浏览器提供的各种服务,这些服务有很多都是在HTML5中定义的,HTML5和相关的标准为Web应用定义了很多其他重要的API,这些API包括以上所说的网络、图像和数据存储,还包含地理位置信息、历史管理和后台线程等,这些都是典型的Web应用;例如XMLHttpRequest对象,其可以发出HTTP请求,可以从服务器端获取新信息,而不用重新载入整个页面,这样的Web应用称为Ajax应用;并且它们可以离线操作,以及保存数据到本地,以便再次访问时进行状态恢复;
Javascript在Web应用里会比在Web文档里显得更加重要;Javascript增强了Web文档,但是设计良好的文档需要 在禁用Javascript后还能继续工作;Web应用本质上就是Javascript程序,只不过使用了Web浏览器提供的服务,如果禁用了Javascript,Web应用就运行不了;
在真实的场景中,并不是完全分离Web文档和Web应用的这两种形式,而是结合了两者的特性;
21 ,浏览器对象模型BOM
BOM(Browser Object Model)浏览器对象模型;其提供了独立于内容而与浏览器窗口进行交互的对象;
js
// 没有BOM标准:不同的浏览器按照各自的想法实现及扩展BOM,于是,它们之间共有的对象成为了事实上的标准;近年来,W3C为了把浏览器中Javascript最基本的部分标准化,已经将BOM的主要方面纳入了HTML5的规范中。
// BOM由一系列相关的对象构成;DOM中各对象之间是层次关系;window对象是整个BOM的核心,所有对象和集合都以某种方式回接到window对象
21-1 window对象
js
// 其是BOM的核心对象,也是顶级对象,表示浏览器的一个实例;
// 浏览器窗口对文档提供一个显示的容器,是每一个加载文档的父对象;window对象表示整个浏览器窗口,但不表示其中所包含的内容;可以用于移动或调整浏览器的大小,或者产生其他的影响;
21-2 全局作用域
在浏览器中,window对象具有双重角色,即是通过Javascript访问浏览器的一个接口,又是ES中的Global对象;
既然window是Global对象,所以window对象是所有其他对象的顶级对象,在网页中定义的任何对象、变量和函数,window对象都有权访问;即所有在全局作用域中声明的变量、函数都会变成window对象的属性和方法;因此window前缀可以省略;
js
var age = 18;
function sayAge(){
alert(this.age);
}
alert(window.age);
sayAge();
window.sayAge();
定义全局变量与在window对象上直接定义属性还是有一点差别:全局变量不能通过delete操作符删除,而直接在window对象上的定义的属性可以,如:
js
var age = 18;
window.color = "green";
console.log(delete window.age); // false
console.log(delete window.color); // true
console.log(window.age); // 18
console.log(window.color); // undefined
age属性的特性中的[[Configurable]],值为false,因此其不能通过delete删除;
js
console.log(Object.getOwnPropertyDescriptor(window,"age"));
console.log(Object.getOwnPropertyDescriptor(window,"color"));
尝试访问未声明的变量会抛出错误,但通过查询window对象,可以知道某个可能未声明的变量是否存在,如:
js
var age = oldAge; // 抛出错误
var age = window.oldAge; // 不会抛出错误,因为这是一次属性查询
21-3 文档元素
如果在HTML文档中用id属性来元素命名,并且如果window对象没有此名字的属性,window对象会赋予一个属性,它的名字是id属性的值,而它们的值指向表示文档元素的HTMLElement对象,这称为全局变量的隐式应用;
在实际场景中,很少使用这种方式来访问HTML元素,它是Web浏览器发展过程中遗留的一个现象,是现代浏览器向后兼容性的考虑,如:
js
<button id="okay">按钮</button>
<input id="myinput" value="input" />
<div id="mydiv">mydiv</div>
<script>
var ui = ["okay","myinput"];
ui.forEach(function(id){
ui[id] = document.getElementById(id);
});
console.log(ui.okay);
console.log(ui.myinput);
// 定义一个更简单的方法
var $ = function(id){
return document.getElementById(id);
};
$("mydiv").innerHTML = "零点网络";
console.log($("mydiv"));
</script>
21-4 窗口位置
获取窗口(视口)的位置(即相对于屏幕左边和上边的位置);各浏览器都实现了screenLeft和screenTop属性表示窗口的位置;但是之前的firefox并不支持,现代firefox返回-8;只有IE是文档区相对于主显示器屏幕的位置;
Firefox使用了screenX和screenY属性获取窗口位置信息;各浏览器也实现了这两个属性,但并不统一:
chrome与Opera实现了screenLeft、screenTop与screenX、screenY的统一;且最大化时,值为0;Firefox与IE实现了统一,但最大化时,值为-8;且Firefox中的screenLeft、screenTop与这两个属性实现了对应;但IE的screenLeft、screenTop与这两个属性并不对应;screenLeft、screenTop是文档区域的左上角的坐标,screenX、screenY是窗口的左上角坐标;
js
console.log(window.screenLeft);
console.log(window.screenTop);
console.log(window.screenX);
console.log(window.screenY);
// 为了兼容老的Firefox
var leftPos = (typeof window.screenLeft == "number") ? window.screenLeft : window.screenX;
var topPos = (typeof window.screenTop == "number") ? window.screenTop : window.screenY;
console.log("leftPos:",leftPos, ",topPos:",topPos);
pageXOffset及pageXOffset:设置或返回当前页面相对于可视区域的左及上的位置;但貌似只有Chrome支持,同时,如果是设置的话,没有效果;
注:目前,无法在跨浏览器的条件下取得窗口左边和上边的精确坐标值;如果在框架中使用,除了IE,其他浏览器的值都与以上统一;
21-5 移动浏览器窗口
- moveBy(dx,dy):相对移动,dx,dy可以为负;
- moveTo(x,y):移动,x,y可以为负;
js
window.moveTo(50,100);
window.moveBy(100,200);
window.moveBy(-50,0);
注:一般很少用;这两个方法有可能会被浏览器禁用;这两个方法都不适用框架,只能对最外层的window对象使用;
21-6 窗口大小
如果要获取浏览器窗口大小信息,各浏览器并不统一;但各浏览器都已实现了以下四个方法,但返回值并不一定相同:
js
- innerWidth和innerHeight // 属性获取视口大小(注:包括body的margin);
- outerWidth和outerHeight // 属性获取浏览器窗口大小(无论是从最外层的window对象还是从某个框架访问);
js
console.log(window.innerWidth);
console.log(window.innerHeight);
console.log(window.outerWidth);
console.log(window.outerHeight);
在各浏览器的实现中,可以通过使用DOM提供的页面视口的相关信息:
document.documentElement.clientWidth和document.documentElement.clientHeight中保存了页面视口的信息;其返回值与innerWidth和innerHeight一致;
在低版本的IE中,必须通过document.body.clientWidth和
document.body.clientHeight属性获取视口信息(实际上是实际内容的尺寸,但不包括border值),但不标准;
同时,document.body.offsetWidth和
document.body.offsetHeight也能获取视口相关信息,同clientWidth和clientHeight类似,只不过其包括border的宽度;
跨浏览器取得页面视口的大小:
js
var pageWidth = window.innerWidth,
pageHeight = window.innerHeight;
if(typeof pageWidth != "number"){
// 判断页面是否处于标准模式
if(document.compatMode == "CSS1Compat"){
pageWidth = document.documentElement.clientWidth;
pageHeight = document.documentElement.clientHeight;
}else{
pageWidth = document.body.clientWidth;
pageHeight = document.body.clientHeight;
}
}
console.log(pageWidth);
console.log(pageHeight);
21-7 调整浏览器窗口的大小:
- resizeBy(dw,dh):相对缩放,dw,dh可以为负;
- resizeTo(w,h):缩放到, w,h不能为负;
注:一般很少用;也有可能被禁用,同移动窗口类似
js
window.resizeTo(400,300);
window.resizeBy(200,100);
21-8 滚动窗口(如果有滚动条)
- scrollBy(x, y):按照指定的像素值相对来滚动的内容(第一个参数是滚动条向右滚动,第二个参数是滚动条向下滚动,方法执行重复执行,值会累加);(可直接在控制台上演示)
- scrollTo(x, y):把内容滚动到指定的坐标;
- scroll(x, y):把内容滚动到指定的坐标;
- scrollX及scrollY:
21-9 对话框:
window对象通过alert()、confirm()和prompt()三个方法可以调用系统对话框向用户显示消息;
系统对话框与在浏览器中显示的网页没有关系,也不包括HTML;它们的外观由操作系统或浏览器设置决定的,而不是由CSS决定的;此外,通过这几个方法打开的对话框都是同步和模态的,也就是说,显示这些对话框的时候代码会停止执行,而关掉这些对话框后代码又会恢复执行;
一般来说,不会使用,都是自己实现一个效果;
警告对话框window.alert(string):
警告对话框是一个带感叹图标的小窗口,通常用来输出一些简单的文本信息;该方法接受一个字符串并将其显示给用户,并包含一个OK或确定的按钮等待用户关闭对话框;
通过alert()生成的警告对话框,用户是无法控制的,比如,显示的消息内容,用户只能看完消息后关闭对话框;
确认对话框window.confirm(string):
确认对话框也是向用户显示消息的对话框,但与警告对话框不同的是,其具有OK与Cancle按钮,根据用户的选择,该方法返回不同的值(true或false);程序可以根据不同的值予以不同的响应,实现互动的效果;通常放在网页中,对用户进行询问并根据其选择而做不同的控制;
js
if(window.confirm("确定删除吗?")){
alert("已经删除");
}else{
alert("未删除");
}
输入对话框window.prompt():
用于提示用户输入的对话框;
语法: window.prompt(提示信息,默认值);// 要显示的文本提示和文本输入域的默认值,该默认值可以是一个空字符串;
js
prompt("请输入你的名字","王唯");
如果用户单击了OK,则prompt()返回文本域中的值,如果单击了Cancel或使用其他方式关闭对话框,则该方法返回null,如:
var result = prompt("请输入密码:","");
if(result == "8888"){
alert("登录成功");
}
var result = prompt("请输入你的名字","王唯");
if(result !== null){
document.write("欢迎你:" + result);
}else{
alert("你没有输入任何内容");
}
综上所述,这些系统对话框很适合向用户显示消息并由用户作出决定;由于不涉及HTML、CSS或JS,因此它们是增强Web应用程序的一种便捷方式;
js
do{
var name = prompt("输入你的名字:");
var correct = confirm("你输入的是:" + name + ".\n" + "你可以单击确定或取消");
}while(!correct)
alert("你好," + name);
在弹出对话框时,还有一个特性:如果当前脚本在执行过程中会打开两个或多个对话框,那么从第二个对话框开始,每个对话框中都会显示一个复选框,以便用户阻止后续的对话框显示,除非用户刷新页面;后续被阻止的对话框包括警告框、确认框和显示输入框;
该特性最初是由Chrome实现了,后续其他浏览器也实现该特性;其工作原理就是使用了一个叫对话框计数器,可以跟踪对话框;但是浏览器没有就对话框是否显示向开发人员提供任何信息;
在实际的场景中,这三个方法是很少用的,因为它们显示的文本是纯文本,不是HTML格式的文本,只能使用空格、换行符和各种标点符号,所以往往满足不了页面设计需要,并且有可能会破坏用户的浏览体验;常见的就是使用alert()方法进行调试,用来查看某个变量的输出结果;
这三个方法都会产生阻塞,也就是说,在用户关掉它们之前,它们不会返回,后面的代码不会执行;如果当前载入文档,也会停止载入,直到用户要求的输入进行响应为止;
可以自定义一个对话框:
js
<style>
#alert_box{
position: absolute; display: none; width: 400px; height:300px; border-radius: 3px;
box-shadow: 0 0 5px rgba(0, 0, 0, .5);
}
#alert_box h1{
margin:0; padding: 0; font-size: 1.5em; line-height: 60px;
height: 60px;
text-align: center; background-color: rgba(255,255,255,1);
}
#alert_box div{
height: 240px;
padding: 1em; line-height: 1.8em; background-color: rgba(255,255,255,.8);
}
#alert_box span{
position: absolute; width: 30px; height: 30px;
top:-15px; right:-15px; background-color:#000; border-radius: 50%;;
}
</style>
<script>
window.alert = function(title,info){
var box = document.createElement("div");
box.id = "alert_box";
box.style.left = ((window.innerWidth - 400) / 2) + "px";
box.style.top = ((window.innerHeight - 300) / 2) + "px";
var h1 = document.createElement("h1");
h1.innerText = title;
box.appendChild(h1);
var innerBox = document.createElement("div");
innerBox.innerHTML = info;
box.appendChild(innerBox);
var closeSpan = document.createElement('span');
box.appendChild(closeSpan);
closeSpan.addEventListener("click",function(e){
document.body.removeChild(box);
},false);
box.style.display = "block";
document.body.appendChild(box);
};
window.alert("零点网络","哪些是这样的?");
</script>
Javascript还有两个工具性的对话框,即查找window.find()和打印window.print();
这两个对话框都是异步显示的,能够将控制权立即交还给脚本;其与浏览器菜单中的查找和打印命令是相同的;
这两个方法同样不会就用户对对话框中的操作给出任何信息,因此它们的用处有限;另外,既然这两个对话框是异步显示的,对话框计数器就不会将它们计算在内,所以它们也不会受用户禁用后续对话框显示的影响;
21-10 状态栏
js
// 浏览器的状态栏通常位于窗口的底部,用于显示一些任务状态信息等。在通常情况下,状态显示当前浏览器的工作状态或用户交互提示信息; 具有status和defaultStatus属性;
// 默认状态栏信息:
// defaultStatus属性可以用来设置在状态栏中的默认文本,是一个可读写的字符串;
// 状态栏瞬间信息:
// status属性,在默认情况下,将鼠标放在一个超链接上时,状态栏会显示该超链接的URL,此时的状态栏信息就是瞬间信息;当鼠标离开超链接时,状态栏就会显示默认的状态栏信息,瞬间信息消失 。
// 浏览器已经关闭了状态栏的功能;这是出于安全的考虑,防止隐藏了超链接真正目的的钓鱼攻击
22,计时器setTimeout和setInterval
22-1 计时器
Javascript是单线程语言,但它允许通过设置超时和间歇时间值来调度代码在特定的时刻执行;其是通过setTimeout()和setInterval()两个window对象的全局函数实现的,用来注册在指定的时间之后单次或重复调用的函数;
22-1-1 setTimeout()
延迟代码执行(也叫超时调用):用来实现一段代码在指定的毫秒之后运行;
语法:window.setTimeout(code,delay),code要执行的代码,可以是一个包含Javascript的代码字符串,也可以是一个函数,delay等待的毫秒数;
JS
// 不建议传递字符串
setTimeout("alert('zeronetwork')",3000);
// 推荐的使用方式
setTimeout(function(){
alert('zeronetwork');
},3000);
// 推荐的使用方式
setTimeout(show,3000);
function show(){
alert('zeronetwork');
}
因为历史原因,第一个参数可以传递字符串,但有可能导致性能损失,因为这个字符串会在指定的超时时间之后进行求值,相当于执行eval(),因此不推荐使用字符串的形式;
JS
// 能达到无限循环的目的
var n = 0;
function fun(){
n++;
console.log(n);
setTimeout(fun, 1000);
}
fun(); // 直接执行
setTimeout(fun,3000);
第二个参数是一个表示等待多长时间的毫秒数,但经过该时间后指定的代码并不一定会执行;Javascript是一个单线程的解释器,因此一定时间内只能执行一段代码;为了控制要执行的代码,就有一个Javascript任务队列,这些任务会执照将它们添加到队列的顺序执行;这个参数实际上是告诉Javascript再过多长时间把当前任务添加到队列中,如果队列是空的,那么添加的代码会立即执行,如果队列不是空的,那么它就要等前面的代码执行完毕后再执行;
另外,如果该参数为0,也并不一定会立即执行,因为也需要将它放到队列中,等待前面的任务全部执行完后,才会"立即"执行;
setTimeout()方法会返回一个数字ID,ID本质上是要延迟进程的ID,是计划执行代码的唯一标识符;可以使用clearTimeout()方法,调用此ID,达到取消超时调用的目的;
js
var timeoutid = setTimeout(function(){
alert("zeronetwork");
},3000);
console.log(timeoutid);
clearTimeout(timeoutid);
只要是在指定的时间尚未过去之前调用clearTimeout(),就可以完全取消超时调用;
js
<input type="button" value="开始" onclick="showClock()" />
<input type="button" value="取消" onclick="window.clearTimeout(ident)" />
<div id="showtime">time</div>
<script>
function showClock(){
var d = new Date();
var showtime = document.getElementById("showtime");
showtime.innerHTML = d.toLocaleString();
// ident = setTimeout(showClock(), 1000);
ident = setTimeout("showClock()", 1000);
}
</script>
示例:可以利用 clearTimeout() 方法在特定条件下清除延迟处理代码。例如,当鼠标指针移过某个元素,停留半秒钟之后才会弹出提示信息,一旦鼠标指针移出当前元素,就立即清除前面定义的延迟处理函数,避免干扰;
js
<h1>零点网络</h1>
<div>零点教育是从事IT教育</div>
<p>主讲:零点网络</p>
<script>
var o = document.getElementsByTagName('body')[0].childNodes;
for(var i=0; i<o.length; i++){
o[i].onmouseover = function(i){
return function(){
f(o[i]);
}
}(i);
o[i].onmouseout = function(i){
return function(){
clearTimeout(o[i].out);
}
}(i);
}
function f(o){
o.out = setTimeout(function(){
console.log(o.tagName + ":" + o.innerText);
},500);
}
</script>
除前两个参数之外,HTML5规范还允许setTimeout()传入额外的参数,并在调用函数时把这些参数传递过去;
js
setTimeout(function(str,age){
alert(str + "age:" + age);
},3000,"wangwei",18);
22-1-2 setInterval()
代码延迟执行机制在执行一次后就失效,而在应用中,有时希望某个程序能反复执行,比如说倒计时等,需要每秒执行一次;为此可以使用window方法的setInterval方法,其会按照指定的时间间隔重复执行代码,直到取消或页面被卸载;其与setTimeout()类似,参数也一致;
js
// 不建议使用字符串
setInterval("console.log('zero')", 3000);
// 推荐的方式
setInterval(function(){
console.log('zero');
},3000);
function timer(){
var d = new Date();
document.getElementById("result").innerText = d.toLocaleTimeString();
}
setInterval(timer,1000);
// 输出的时间并不精确,并不是整1000毫秒
var firstTime = new Date().getTime();
setInterval(function(){
var lastTime = new Date().getTime();
console.log(lastTime - firstTime);
// alert("ok"); // 会暂停
firstTime = lastTime;
},1000);
同setTimeout()一样,setInterval()也支持第三个参数;
取消间隔性执行代码:
使用setInterval()方法同样会返回一个间隔调用ID,该ID可用于在将来某个时间取消间隔调用;可以使用clearInterval方法移除间隔调用,其接收一个计时器ID作为参数;
js
// 如果不使用它的返回值,可以直接使用数字1、2...
var i = 0;
setInterval(function(){
console.log(i++);
if(i>10)
clearInterval(1);
},1000);
取消间隔调用的重要性要远远高于取消超时调用,因为在不取消的情况下,间隔调用将会一直执行到页面卸载;
js
var num = 0, max = 10;
var intervalId = null;
function incNum(){
num++;
console.log(num);
// 如果执行次数达到了max设定的值,则取消后续的调用
if(num == max){
clearInterval(intervalId);
alert("结束");
}
}
intervalId = setInterval(incNum, 1000);
// 另外
var mInput = document.getElementsByTagName('input')[0];
var sInput = document.getElementsByTagName('input')[1];
var m = 4,s = 52;
var timer = setInterval(function(){
s++;
if(s == 60){
s = 0;
m++;
}
sInput.value = s;
mInput.value = m;
if(m == 5)
clearInterval(timer);
},1000);
setTimeout()与setInterval()同时使用时,其返回的id也会按顺序返回;
在某些时候 setTimeout()与 setInterval() 可以实现同样的效果;
js
var num = 0, max = 10;
function incNum(){
num++;
console.log(num);
// 如果执行次数达到了max设定的值,则取消后续的调用
if(num < max){
setTimeout(incNum, 1000)
}else{
alert("结束");
}
}
setTimeout(incNum, 1000);
在使用超时调用时,没有必要使用超时调用ID,因为每次执行代码之后,如果不再设置另一次超时调用,调用就会自动停止;
一般认为,使用延迟代码来模拟时间间隔是一种最佳方式;在开发环境中,很少使用时间间隔,因为时间间隔可能会在前一个间隔调用结束之前启动,而延迟代码完全可以避免这一点;
js
<div id="loadBar" style="border: red 1px solid;"></div>
<script>
var num = 0;
var colors = ['#494949','#646464','#747474','#888888','#969696','#A8A8A8','#B6B6B6','#C6C6C6','#D7D7D7','#E1E1E1','#F0F0F0','#F9F9F9'];
function loading(){
num++;
var loadBar = document.getElementById("loadBar");
loadBar.style.color = colors[num-1];
loadBar.innerHTML = loadBar.innerHTML + "■";
if(num < 12){
setTimeout(loading, 1000);
}else{
loadBar.style.display = "none";
window.open("https://www.zeronetwork.cn/","new");
}
}
window.onload = loading;
</script>
/*
定时器应用函数 invoke
如果只传递f,start,则使用setTimeout
如果没有传递end,则永久循环执行f,否则在end后停止
*/
function invoke(f, start, interval, end){
if(!start) start = 0; // 默认设置为0毫秒
if(arguments.length <= 2) // 单次调用模式
setTimeout(f, start);
else{ // 多次调用模式
setTimeout(repeat, start); // 若干秒后调用repeat()
function repeat(){
var h = setInterval(f, interval); // 循环调用f()
// 在end毫秒后停止调用,前提是end已经定义了
if(end){
setTimeout(function(){
clearInterval(h);
}, end);
}
}
}
}
invoke(function(){
console.log("wangwei");
},1000,2000,5000);
23, location、history对象
23-1 location对象
JS
// location对象,是BOM最有用的对象之一了,是window的属性(子对象),它提供了与当前窗口中加载文档的URL,还提供一些导航及载入文档的方法;
// location对象很特别,即是window对象的属性,也是document对象的属性,即window.location与document.location引用的是同一个对象;
location对象的特点是,它不仅保存着当前文档的信息,它还将URL解析为独立的片段,让开发人员可以通过不同的属性访问这些片段;
23-1-1 属性
js
- hash:// 返回URL中的hash,即井号 (#)后跟的多个字符,即URL中的锚,如果没有,则返回空字符串;如:#name;
- host:// 返回主机名和端口号(如果有),如:www.zeronetwork.cn:80;
- hostname:// 返回当前主机名不包括端口号,如:www.zeronetwork.cn;
- origin:// 只读,返回当前协议、主机名和端品号,如:https://www.zeroentwork.cn:80;
- href:// 返回当前加载页面的完整URL,location对象的toString()也返回这个值,如:https://www.zeronetwork.cn;
- pathname:// 返回当前 URL 的路径部分,即目录和文件名,如:/edu/index.html;
- port:// 返回当前 URL中的端口号,如果没有,则返回空字符串,如:8080;
- protocol:// 返回当前 URL 的协议,通常是http或https;
- search:// 返回从问号 (?) 开始的查询字符串,如:?q=wangwei
23-1-2 方法
js
- assign():// 加载新的文档;
- reload():// 重新加载当前文档;
- replace():// 用新的文档替换当前文档;
23-1-3 解析URL
location对象的href属性是一个字符串,包含URL的完整文本;location对象的toString()方法返回href属性的值,因此在会隐匿调用toString()的情况下,可以使用location代替location.href;
js
console.log(location);
console.log(location.href);
console.log(location.toString());
console.log(location + ""); // 字符串
// 可以直接赋值,即为location.href = url
location = "https://www.zeronetwork.cn";
<!-- 页面跳转 -->
<p>页面会在<span id="result"></span>秒后跳转到:https://www.zeronetwork.cn</p>
<script>
var n = 5;
result.innerText = n;
setInterval(function(){
n--;
if(n==0)
location.href = "https://www.zeronetwork.cn/";
else
result.innerText = n;
},1000);
</script>
其他的属性都表示URL的各个部分,它们被称为"URL分解"属性,同时被Link对象(html中的和元素)支持;这些属性都是可写的;
js
// 获取页面名
var pathname = location.pathname;
var pagename = pathname.substring(pathname.lastIndexOf('/')+1);
console.log(pagename);
23-1-4 查询字符串参数
获取参数可以通过Location对象的search属性,获得从URL中传递过来的参数和参数值,但不能逐个访问参数,因此需要单独解析查询字符,用以处理需要获取的参数和参数值;
js
function getQueryString(){
// 取得查询字符串并去掉开头的问
var qs = location.search.length > 0 ? location.search.substring(1) : "";
// 保存数据的对象
var args = {};
// 取得每一项
var items = qs.length ? qs.split("&") : [];
var item = null, name=null, value = null;
var i=0, len=items.length;
// 逐个将每一项添加到args对象中
for(i=0; i<len; i++){
item = items[i].split("=");
name = decodeURIComponent(item[0]);
value = decodeURIComponent(item[1]);
if(name.length){
args[name] = value;
}
}
return args;
}
// 应用,假定为:?q=zeronetwork&num=100
var args = getQueryString();
alert(args["q"]); //zeonetwork
alert(args["num"]); // 100
23-1-5 加载新文档
在实际场景中,时常会用到加载一个新的网页的情况;此时可以用Location对象的href属性就可以轻松完成这一功能,该属性返回值为当前文档的URL,如果将该属性值设置为新的URL,那么浏览器会自动加载该URL的内容,从而达到加载一个新的网页的目的;
js
console.log(location.href);
location.href = "https://www.zeronetwork.cn/";
// 或者直接为location赋新的URL值
window.location = "https://www.zeronetwork.cn/";
如此,就能立即打开新的URL并在浏览器的历史记录中生成一条记录;
修改location对象的其他属性也可以改变当前加载的页面,如:
js
// 注意先后顺序
location.pathname = "/edu/index.html";
location.hash = "#selection1";
location.search = "?q=wangwei";
location.port = "8080";
location.hostname = "www.zeronetwork.cn";
function gotoUrl(){
window.location.href = "http://www.zeronetwork.cn";
}
// 传递参数
function gotoUrl(url,catalogid){
var url = url + "?catalogid=" + catalogid;
window.location = url;
}
function gotoUrl(url,catalogid){
if(catalogid <= 0)
location = url;
else
location = url + "?catalogid=" + catalogid;
}
每次修改location的属性(hash除外),页面都会以新的URL重新加载;
修改hash有可能不会在历史记录中生成一条记录;
js
<p id="info"></p>
<script>
var s = 5;
var info = document.getElementById("info");
function go(){
if(s==0)
window.location.href = "https://www.zeronetwrok.cn/";
else
info.innerHTML = "浏览器将在" + s + "后跳转";
s--;
}
setInterval(go, 1000);
</script>
Document对象也有一个URL属性,是文档首次载入后保存该文档的URL的字符串;
console.log(location.href == document.URL);
23-1-6 装载新文档与重新装入当前文档
文档的装载在应用中也是比较常见的,有三个方法:assign、replace和reload;
js
// 使用按钮演示
var mybtn = document.getElementById("mybtn");
mybtn.addEventListener("click",function(){
// ...
});
23-1-7 assign()方法
使窗口载入并显示指定的URL中的文档;
js
// window.location=URL与location.href=URL本质上都是调用了assign()方法;三者是等同的用法;
// location.assign("https://www.zeronetwork.cn/");
23-1-8 replace()方法
以上的方式修改URL后,在浏览器的历史记录中会生成一条新记录,因此用户通过单击"后退"按钮,都会导航到前一个页面;要禁用这种行为,可以使用replace()方法;该方法只接受一个URL参数,并不会在历史记录中生成新记录;
js
location.replace("https://www.zeronetwork.cn/");
// 如果浏览器不支持XMLHttpRequest对象,则重定向一个不需要Ajax的页面
if(!XMLHttpRequest) location.replace("noajax.html");
注:relace()的URL参数可以是绝对或相对的URL;
23-1-9 reload()方法
该方法用于根据浏览器reload按钮定义的策略重新加载当前窗口的文档;
reload()方法直接有可能从缓存中重新加载,如果加入参数true,会从服务器重新加载;
js
window.location=URL与location.href=URL本质上都是调用了assign()方法;三者是等同的用法;
location.assign("https://www.zeronetwork.cn/");
位于reload()方法调用之后的代码可能会也可能不会执行,这要取决于网络延迟或系统资源等因素,所以,reload()方法最好放到代码的最后一行;
23-2 history历史对象
history浏览历史对象并不常用,是window对象的属性,其保存着用户浏览的历史记录,每个窗口,标签页以及每个框架,都有自己的history对象与特定的window对象关联;
当页面的URL改变时,就会生成一条历史记录;
但通过使用history对象可以获知浏览器窗口近来访问过的网页个数,还可以实现从一个页面跳到另一个页面,在实际应用中,如涉及到页面的跳转问题,可以用这个对象来解决;
23-2-1 go(n)方法
表示前进或后退; 参数是一个整数值,为正则前进,为负为后退; 如:
js
history.go(-1); // 后退一页
history.go(1); // 前进一页
history.go(2); // 前进2页
也可以传历史记录中的字符串,如:
history.go("baidu.com");
此时浏览器会跳转到历史记录中包含该字符串的第一个记录,有可能后退,也可能前进,具体要看哪个位置最近;如果历史记录中不包含该字符串,此时该方法什么也不做;
23-2-2 back()和forward()方法
前进和后退,是go()的简写方法,如:
js
history.back(); // 后退一页
history.forward(); // 前进一页
也可以创建自定义的前进和后退功能;
23-2-3 length属性
获取历史记录中的数量;对于加载到窗口、标签页或框架中的第一个页面而言,其值为0;
js
alert(history.length);
// 判断用户是不是打开窗口后第一个加载此页面
if(history.length == 0){
alert("第一个打开的页面");
}
如果窗口包含多个子窗口,子窗口的浏览历史会按时间顺序穿插在主窗口的历史中;这意味着在主窗口调用history.back()等方法可能会导致其中一个子窗口跳转,但主窗口保留当前状态不变;
23-2-4 history.pushState()和history.replaceState()
HTML5为history对象添加了两个新方法:history.pushState()和history.replaceState(),用来在浏览历史中添加和修改记录;
state属性用来保存记录对象,而popstate事件用来监听history对象的变化;
history.pushState()方法向浏览器历史添加了一个状态;其可以传三个参数:一个状态对象、一个标题(现在被忽略了)以及一个可选的URL地址,如:history.pushState(state, title, url);
state:状态对象是一个由pushState()方法创建的、与历史纪录相关的javascript对象;如果不需要这个对象,此处可以填null;
title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null;
URL:这个参数提供了新历史纪录的地址;新URL必须和当前URL在同一个域,否则,pushState()将丢出异常。这个参数可选,如果它没有被特别标注,会被设置为文档的当前URL;
js
var stateObj = {foo:'bar'}
history.pushState(stateObj,"new page 1", 'one.html');
pushState方法不会触发页面刷新,只是导致history对象发生变化,地址栏的显示地址发生变化;
如果pushState的url参数,设置了一个新的锚点值(即hash),并不会触发hashchange事件,即使新的URL和旧的只在hash上有区别;
如果设置了一个跨域网址,则会报错;这是由于同源策略限制;
history.replaceState方法的参数与pushState方法一模一样,不同之处在于replaceState()方法会修改当前历史记录条目而并非创建新的条目;
js
history.pushState({page:1},"title 1", '?page=1');
history.pushState({page:2},"title 2", '?page=2');
history.replaceState({page:3},"title 3", '?page=3');
// 显示http://127.0.0.1:5500/sub/client.html?page=1
history.back();
// 显示http://127.0.0.1:5500/sub/client.html
history.back();
// 显示http://127.0.0.1:5500/sub/client.html?page=3
history.go(2);
history.state属性返回当前页面的state对象
history.pushState({page:3},'title 3','?page=3');
console.log(history.state);
24,navigator、screen对象
24-1 navigator对象
通过navigator对象,可以获取当前使用的是上面浏览器,浏览器的版本号,浏览器是否支持Java,当前浏览器有哪些插件(Plug-ins)可用等信息,根据这些信息可以定制一些自定义行为或进行相应的处理;
navigator对象是window对象的属性;
最早由Navigator2.0引入的navigator对象,现已成为识别客户端浏览器的事实标准;一般用来检测浏览器及操作系统;
其他浏览器也通过其他方式提供了相同或相似的信息,如,IE中的window.clientInformation和Opera的window.opera,但navigator对象是所有支持javascript的浏览器所共有的;但不同浏览器中的navigator对象都有一些不同的属性;
JS
var sum=0;
for(var p in navigator){
sum++;
document.write(p + "<br/>");
}
document.write(sum);
24-1-1 属性或方法
- **appCodeName:**浏览器的名称;通常是Mozilla,即使在非Mozilla也是如此
- **appMinorVersion:**次版本信息
- **appName:**完整的浏览器名称;
- **appVersion:**浏览器的版本;一般不与实际的浏览器版本对应;
- **builddID:**浏览器编译版本
- **cookieEnabled:**表示cookie是否启用
- **cpuClass:**客户端计算机中使用的cpu类型(x86、68K、Alpha、PPC或Other)
- **javaEnabled():**表示当前浏览器是否启用了java
- **language:**浏览器的主语言
- **mimeTypes:**在浏览器中注册的MIME类型数组
- **online:**表示浏览器是否连接到了因特网
- **opsProfile:**已不使用
- **oscpu:**客户端计算机的操作系统或使用的CPU
- **platform:**浏览器所在的系统平台
- **plugins:**浏览器中安装的插件信息的数组
- **preference():**设置用户的首选项
- **product:**产品名称,如Gecko
- **productSub:**关于产品的次要信息,如Gecko的版本
- **register-ContentHandler():**已经废弃;针对特定的MIME类型将一个站点注册为处理程序
- **register-ProtocolHandler():**针对特定的协议将一个站点注册为处理程序
- **securityPolicy:**已经废弃;安全策略的名称
- **systemLanguage:**操作系统的名称
- **taintEnabled():**已经废弃;表示是否允许变量被修改
- **userAgent:**浏览器的用户代理字符串
- **userLanguage:**操作系统的默认语言
- **userProfile:**借以访问用户个人信息的对象
- **vendor:**浏览器的品牌
- **verdorSub:**有关供应商的次要信息,这些属性通常用于检测显示网页的浏览器类型;
浏览器嗅探在有些时候还是必要的,比如,当需要解决存在于某个特定的浏览器的特定版本中的特殊的bug时,此时就可以使用navigator某些属性来获取当前浏览器的版本信息,比如:
appName:Web浏览器的全称;在低版本的IE中,返回"Microsoft Internet Explorer";在Firefox等其它浏览器中,返回"Netscape";
appVersion:此属性通常以数字开始,并跟着包含浏览器厂商和版本信息字符串;字符串前面的数字通常是4.0或5.0,表示它是第4或第5代兼容的浏览器;appVersion字符串没有标准的格式,所以,没有办法直接用它来判断浏览器的类型;
userAgent:浏览器在HTTP头部中发送的字符串,该属性通常包含appVersion中的所有信息,并且常常也可能包含其他的细节;它也没有标准格式;但由于它包含浏览器的绝大部分信息,因此浏览器嗅探通常使用它;
platform:操作系统平台的字符串;
24-1-2 检测浏览器内核及版本号
JS
// 检测浏览器内核及版本号
var brower = (function(){
var s = navigator.userAgent.toLowerCase();
var match = /(webkit)[\/]([\w.]+)/.exec(s) ||
/(opera)(?:.*version)?[ \/]([\w.]+)/.exec(s) ||
/(msie) ([\w.]+)/.exec(s) ||
!/compatible/.test(s) &&
/(mozilla)(?:.*? rv:([\w.]+))?/.exec(s) ||
[];
return {name: match[1] || "", version: match[2] || "0"};
}());
console.log(navigator.userAgent);
console.log(brower);
24-1-3 检测操作系统
所有 Windows 版本的操作系统都会包含 "Win"字符串,所有 Macintosh 版本的操作系统都会包含"Mac"字符串,所有 Unix 版本的操作系统都会包含有"X11",而 Linux 操作系统会同时包含"X11"和"Linux";
["Win","Mac","X11","Linux"].forEach(function(t){
(t == "X11") ? t = "Unix" : t; // 处理Unix系统
// 为navigator对象扩展专用系统检测方法
navigator["is" + t] = function(){
return navigator.userAgent.indexOf(t) != -1;
};
});
console.log(navigator.isWin());
console.log(navigator.isMac());
console.log(navigator.isLinux());
console.log(navigator.isUnix());
24-1-4 特征检测法
特征检测法就是根据浏览器是否支持特定的功能来决定相应操作的方式;这是一种非精确判断法,但却是最安全的检测方法,因为准确检测浏览器的类型和型号是一件很困难的事情,而且很容易存在误差;如果不关心浏览器的身份,仅仅在意浏览器的执行能力,那么使用特征检测法就完全可以满足需要;
JS
if(document.getElementsByName)
var a = document.getElementsByName("test");
else if(document.getElementsByTagName)
var a = document.getElementsByTagName("a");
console.log(a);
function getXMLHttpRequest(url){
var xhr = null;
if(window.XMLHttpRequest)
xhr = new XMLHttpRequest();
else
xhr = new ActiveXObject("Microsoft.XMLHTTP");
if(xhr != null){
// 开始请求提交
alert("提交成功");
}else{
alert("你的浏览器不支持XMLHTTP");
}
}
getXMLHttpRequest();
当使用一个对象、方法或属性时,先判断它是否存在;如果存在,则说明浏览器支持该对象、方法或属性,那么就可以放心使用;这是目前最主流的一种检测方式;
除了浏览器厂商和版本信息的属性之外,navigator对象还包含一些杂项的属性和方法,如以下这些属性,已经得到广泛的应用但未标准化;
- onLine:表示浏览器当前是否连接到网络;应用程序可能希望在离线状态下把状态保存到本地;
- geolocation:用于确定用户地理位置信息的API;
- javaEnabled():一个非标准的方法,当浏览可以运行Java小程序时返回ture;
- cookieEnable():非标准的方法,如果浏览器可以保存永久的cookie时,返回true,其它返回false;
24-1-5 检测插件
检测浏览器中是否安装了特定的插件是一种最常见的检测例程;
对于非低版本的IE可以使用plugins数组来达到目标;该数组中的每一项包含以下属性:
- name:插件的名字;
- description:插件的描述;
- filename:插件的文件名;
- length:插件所要处理的MIME类型数量;
一般来说,name属性中会包含检测插件必需的所有信息,但有时也完全如此;在检测插件时,需要迭代plugins数组中的每个插件,如:
JS
// 检测非低版本的IE插件
function hasPlugin(name){
name = name.toLowerCase();
for(var i=0,len=navigator.plugins.length; i<len; i++){
if(navigator.plugins[i].name.toLowerCase().indexOf(name) > -1)
return true;
}
return false;
}
// 检测Flash
alert(hasPlugin("Flash"));
// 检测QuickTime
alert(hasPlugin("QuickTime"));
// 检测Java
alert(hasPlugin("Java"));
每个插件本身也是一个MimeType对象的数组,这些对象可以通过方括号语法来访问,共有四个属性:MIME类型描述description、回指插件对象enabledPlugin、表示与MIME类型对应的文件扩展名的字符串suffixes、表示完整MIME类型字符串type;
检测IE中的插件比较麻烦,因为IE不支持Netscape式的插件;在IE中检测插件的唯一方式就是使用专有的ActiveXObject类型,并尝试创建一个特定插件的实例;IE是以COM对象的方式实现插件的,而COM对象使用唯一标识符来标识;因此,要想检查特定的插件,就必须知道其COM标识符,如:Flash的标识符是:
ShockwaveFlash.ShockwaveFlash;
js
// 检测IE中的插件
function hasIEPlugin(name){
try{
new ActiveXObject(name);
return true;
}catch(e){
return false;
}
}
// 检测Flash
alert(hasIEPlugin("ShockwaveFlash.ShockwaveFlash"));
// 检测QuickTime
alert(hasIEPlugin("QuickTime.QuickTime"));
使用了try、catch,因为创建未知的COM对象会导致抛出错误;
两种检测方法差别太大,典型的作法是针对每个插件分别创建测试函数;如:
js
// 检测所有浏览器中的Flash
function hasFlash(){
var result = hasPlugin("Flash");
if(!result)
result = hasIEPlugin("ShockwaveFlash.ShockwaveFlash");
return result;
}
// 检测所有浏览器中的QuickTime
function hasQuickTime(){
var result = hasPlugin("QuickTime");
if(!result)
result = hasIEPlugin("QuickTime.QuickTime");
return result
}
alert(hasFlash()); // 检测Flash
alert(hasQuickTime()); // 检测QuickTime;
plugins集合中有个refresh() 方法,用于刷新plugins以反映最新安装的插件,这个方法接收一个参数:表示是否应该重新加载页面的一个布尔值;如果为true,重新加载包含插件的所有页面,否则,只更新plugins集合,不重新加载页面;
24-1-6 注册处理程序
registerProtocolHandler()方法接收三个参数:要处理的协议(mailto或ftp)、处理该协议的页面的URL和应用程序名称;如:将一个应用程序注册为默认的邮件客户端:
navigator.registerProtocolHandler("mailto","http://127.0.0.1:5500/?cmd=%s",
"Some Mail Client"); // %s表示原始的请示
IE不支持,并且在生产环境中,几乎没有什么用途;
24-2 screen对象
screen对象提供了获取显示器信息的功能,显示器信息的主要用途是确定网页在客户机是所能达到的最大显示空间;此对象的用处不大,其只是用来获取客户端的能力,其中包括显示器的信息,如宽和高或颜色数量的信息;每个浏览器中的screen对象都包含着各不相同的属性;
24-2-1 属性
- availHeight 和 availWidth:只读,屏幕减去系统部件(比如任务栏)的高度和屏幕减去系统部件的宽度;即实际可用的大小;
- colorDepth:只读,返回颜色位数,如24,多数为32位
- pixelDepth:只读,屏幕的位深(FF)
- width 和 height:屏幕的宽度和屏幕的高度
- left 和 top:当前屏幕距左边的距离和距顶边的距离(FF支持)
- availLeft 和 availTop:只读,未被系统占用的最左侧的和最上方的像素值 (FF)
- bufferDepth:读、写用于呈现屏外位图的位数(IE)
- deviceXDPI与deviceYDPI:只读,实际的水平与垂直DPI(IE)
- logicalXDPI与logicalYDPI:只读,屏幕逻辑的水平与垂直DPI (IE)
- fontSmoothingEnabled:只读,是否启用字体平滑(IE)
- updateInterval:读、写,以毫秒表示的屏幕刷新时间间隔(IE)
这些信息经常出现在测定客户端能力的站点跟踪工具中,但通常不会用于影响功能;不过,有时候也可能会用到其中的信息来调整浏览器窗口的大小,使其占据屏幕的可用空间,如:
js
// 网页全屏,非IE会禁用调整窗口的能力,因此是无效的
window.moveTo(0,0);
window.resizeTo(screen.availWidth, screen.availHeight);
// 弹出窗口居中
function center(url){
var w = screen.availWidth / 2;
var h = screen.availHeight / 2;
// 计算居中显示时左侧坐标
var l = (screen.availWidth - w) / 2;
// 计算居中显示时顶部坐标
var t = (screen.availHeight - h) / 2;
// 计算坐标参数字符串
var p = "top=" + t + ",left=" + l + ",width=" + w + ",height=" + h;
var win = window.open(url, "newin", p);
win.focus();
}
center("https://www.zeronetwork.cn");
涉及移动设备的屏幕大小时,情况有所不同;运行iOS的设备始终会返回设备竖着方向的尺寸768X1024,而Android会相应调用screen.width和screen.height的值;
js
// 网页开屏
var x=0, y = window.screen.availHeight, dx=5;
var newWin, intervalID;
function showPage(){
if(x < screen.availWidth)
x += dx;
else
clearInterval(intervalID);
newWin.resizeTo(x, y);
}
function showWin(){
newWin = window.open("","newWin","menubar=no,toolbar=no");
newWin.moveTo(0,0);
newWin.resizeTo(x,y);
intervalID = window.setInterval(showPage, 100);
}
showWin();
// 网页布局
function loadCSS(){
var iWidth = screen.availWidth;
var sCSSUrl;
switch(iWidth){
case 1024:
sCSSUrl = "style1.css";
break;
case 1280:
sCSSUrl = "style2.css";
break;
default:
sCSSUrl = "default.css";
break;
}
var oCSS = document.createElement("link");
oCSS.setAttribute("rel","stylesheet");
oCSS.setAttribute("type","text/css");
oCSS.setAttribute("href",sCSSUrl);
document.getElementsByTagName("head")[0].appendChild(oCSS);
}
window.onload = loadCSS;
24-2-2 错误处理
window对象的onerror属性是一个事件处理程序,当未捕获的异常传递到调用栈上时就会调用它,并把错误的消息输出到浏览器的Javascript控制台上;
window.onerror的第一个参数是描述错误的一条消息,第二个参数是一个字符串,它存放引发错误的Javascript代码所在的文档的URL,第三个参数是文档中发生错误的行数;
onerror处理程序也有一个返回值,如果返回false,它通知浏览器事件处理程序已经处理了错误,不需要其他操作(换句话说,浏览器不应该显示它自己的错误消息;
onerror处理程序是早期的JavaScript的产物,那时语言核心不包括try/catch异常处理语句;现在实际开发中,虽然很少使用它,但有些项目还在使用它,如:
js
// 在一个对话中弹出错误消息,但不超过三次
window.onerror = function(msg,url,line){
if(onerror.num++ < onerror.max){
alert("ERROR: " + msg + "\nurl: " + url + "\nline: " + line);
return true;
}
}
onerror.max = 3;
onerror.num = 0;
function show(a,b){
return sum(a,b);
}
console.log(show(3,0));
25,面向对象
25-1 ES6 中的类和对象
25-1-1 类的创建
JS
// 语法
class name {
// class body
}
// 创建实例
var xx = new name();
25-1-2 类 constructor 构造函数
constructor() 方法是类的构造函数(默认方法),用于传递参数,返回实例对象,通过 new 命令生成对象实例时,自动调用该方法。如果没有显示定义, 类内部会自动给我们创建一个constructor()
JS
class Person {
constructor(name,age) { // constructor 构造方法或者构造函数
this.name = name;
this.age = age;
}
}
var ldh = new Person('刘德华', 18);
console.log(ldh.name)
25-1-3 给类添加方法
JS
class Person {
constructor(name,age) { // constructor 构造器或者构造函数
this.name = name;
this.age = age;
}
say() {
console.log(this.name + '你好');
}
}
var ldh = new Person('刘德华', 18);
ldh.say()
// 注意: 方法之间不能加逗号分隔,同时方法不需要添加 function 关键
25-2 类的继承
现实中的继承:子承父业,比如我们都继承了父亲的姓。
程序中的继承:子类可以继承父类的一些属性和方法
JS
class Father{ // 父类
}
class Son extends Father { // 子类继承父类
}
25-2-1 继承
JS
class Father {
constructor(surname) {
this.surname= surname;
}
say() {
console.log('你的姓是' + this.surname);
}
}
class Son extends Father{ // 这样子类就继承了父类的属性和方法
}
var damao= new Son('刘');
damao.say();
25-2-2 super 关键字
super 关键字用于访问和调用对象父类上的函数。可以调用父类的构造函数,也可以调用父类的普通函数
JS
lass Person { // 父类
constructor(surname){
this.surname = surname;
}
}
class Student extends Person { // 子类继承父类
constructor(surname,firstname){
super(surname); // 调用父类的constructor(surname)
this.firstname = firstname; // 定义子类独有的属性
}
}
注意: 子类在构造函数中使用super, 必须放到 this 前面 (必须先调用父类的构造方法,在使用子类构造方法)
JS
class Father {
constructor(surname) {
this.surname = surname;
}
saySurname() {
console.log('我的姓是' + this.surname);
}
}
class Son extends Father { // 这样子类就继承了父类的属性和方法
constructor(surname, fristname) {
super(surname); // 调用父类的constructor(surname)
this.fristname = fristname;
}
sayFristname() {
console.log("我的名字是:" + this.fristname);
}
}
var damao = new Son('刘', "德华");
damao.saySurname();
damao.sayFristname();
super关键字 用于访问和调用对象父类上的函数。可以调用父类的构造函数,也可以调用父类的普通函数
JS
class Father {
say() {
return '我是爸爸';
}
}
class Son extends Father { // 这样子类就继承了父类的属性和方法
say() {
// super.say() super 调用父类的方法
return super.say() + '的儿子';
}
}
var damao = new Son();
console.log(damao.say());
25-3 Class类
js
// 以手机类为例
function Phone(brand,price){
this.brand = brand
this.price = price
}
// 静态成员
//相当于 python里的staticmethod
Phone.name = '手机'
Phone.change = function(){
console.log('我可以改变世界')
}
// 相当于Python里的classmethod
Phone.ptototype.size = "5.5英寸"
Phone.prototy.call = function() {
console.log('我可以打电话')
}
ler Huawei = new Phone('华为',5999)
// ES6语法
class Phone{
// 构造函数方法
constructor(brand,price){
this.brand = brand
this.call = call
}
// 语法必须使用这个方法,不能用ES5老语法
call() {
console.log('我可以打电话')
}
}
ler Huawei = new Phone('华为',5999)
25-3-1 构造函数继承
js
function Phone(brand,price){
this.brand = brand
this.price = price
}
Phone.prototype.call = function(){
console.log('我可以打电话')
}
//智能手机
function SmartPhone(brand,price,color,size){
Phone.call(this,brand,price){
this.color = color
this.size = size
}
}
// 设置子级构造函数的原型
SmartPhone.prototype = new Phone
SmartPhone.prototype.constructor = SmartPhone
// 声明子类的方法
SmartPhone.prototype.photo = function(){
console.log('我可以拍照')
}
SmartPhone.prototype.playGame = function() {
console.log('我可以打游戏')
}
const chuzi = new SmartPhone("锤子",2499,'黑色','505')
25-3-2 ES6里类的继承
js
class Phone{
constructor(brand,price){
this.brand = brand
this.price = price
}
call(){
console.log('我可以打电话')
}
}
class SmartPhone extends Phone{
constructor(brand,price,color,size){
super(brand,price)
this.color = color
this.size = size
}
photo() {
console.log('我可以拍照')
}
playGame() {
console.log('我可以打游戏')
}
}
const xioami = new SmartPhone('xiaomi',2499,'黑色','505')
25-3-3 子类对父类方法的重写
js
class Phone{
constructor(brand,price){
this.brand = brand
this.price = price
}
call(){
console.log('我可以打电话')
}
}
class SmartPhone extends Phone{
constructor(brand,price,color,size){
super(brand,price)
this.color = color
this.size = size
}
photo() {
console.log('我可以拍照')
}
playGame() {
console.log('我可以打游戏')
}
// 可以对父类方法重写
call(){
console.log('我可以进行视频通话')
}
}
const xioami = new SmartPhone('xiaomi',2499,'黑色','505')
25-3-4 class中的getter和setter
js
class Phone{
get price() {
console.log('我被读取了')
return 'i love you '
}
set price(val){
console.log('价格修改了')
this.val = val
}
}
let s = new Phone()
console.log(s.price)
// '我被读取了'
// 'i love you ' -------------------> 这才是s.price的返回值
s.price = 'free' // '价格修改了'
26,JavaScript操作DOM
26-1 获取元素
26-1-1 根据ID获取
js
document.getElementById('id');
使用 console.dir() 可以打印我们获取的元素对象,更好的查看对象里面的属性和方法。
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="time">2019-9-9</div>
<script>
// 1. 因为我们文档页面从上往下加载,所以先得有标签 所以我们script写到标签的下面
// 2. get 获得 element 元素 by 通过 驼峰命名法
// 3. 参数 id是大小写敏感的字符串
// 4. 返回的是一个元素对象
var timer = document.getElementById('time');
console.log(timer);
console.log(typeof timer);
// 5. console.dir 打印我们返回的元素对象 更好的查看里面的属性和方法
console.dir(timer);
</script>
</body>
</html>
26-1-2 根据标签名获取
使用 getElementsByTagName() 方法可以返回带有指定标签名的对象的集合。
js
document.getElementsByTagName('标签名');
js
注意:
// 因为得到的是一个对象的集合,所以我们想要操作里面的元素就需要遍历。
// 得到元素对象是动态的
// 如果获取不到元素,则返回为空的伪数组(因为获取不到对象)
还可以获取某个元素(父元素)内部所有指定标签名的子元素.
js
element.getElementsByTagName('标签名');
// 注意:父元素必须是单个对象(必须指明是哪一个元素对象). 获取的时候不包括父元素自己。
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<ul>
<li>知否知否,应是等你好久11</li>
<li>知否知否,应是等你好久11</li>
<li>知否知否,应是等你好久11</li>
<li>知否知否,应是等你好久11</li>
</ul>
<ol id="ol">
<li>生僻字</li>
<li>生僻字</li>
<li>生僻字</li>
<li>生僻字</li>
</ol>
<script>
// 1.返回的是 获取过来元素对象的集合 以伪数组的形式存储的
var lis = document.getElementsByTagName('li');
console.log(lis);
console.log(lis[0]);
// 2. 我们想要依次打印里面的元素对象我们可以采取遍历的方式
for (var i = 0; i < lis.length; i++) {
console.log(lis[i]);
}
// 3. 如果页面中只有一个li 返回的还是伪数组的形式
// 4. 如果页面中没有这个元素 返回的是空的伪数组的形式
// 5. element.getElementsByTagName('标签名'); 父元素必须是指定的单个元素
// var ol = document.getElementsByTagName('ol'); // [ol]
// console.log(ol[0].getElementsByTagName('li'));
var ol = document.getElementById('ol');
console.log(ol.getElementsByTagName('li'));
</script>
</body>
</html>
26-1-3 通过 HTML5 新增的方法获取
js
document.getElementsByClassName('类名');// 根据类名返回元素对象集合
document.querySelector('选择器'); // 根据指定选择器返回第一个元素对象
document.querySelectorAll('选择器'); // 根据指定选择器返回
注意: // querySelector 和 querySelectorAll里面的选择器需要加符号,比如:document.querySelector('#nav');
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div class="box">盒子1</div>
<div class="box">盒子2</div>
<div id="nav">
<ul>
<li>首页</li>
<li>产品</li>
</ul>
</div>
<script>
// 1. getElementsByClassName 根据类名获得某些元素集合
var boxs = document.getElementsByClassName('box');
console.log(boxs);
// 2. querySelector 返回指定选择器的第一个元素对象 切记 里面的选择器需要加符号 .box #nav
var firstBox = document.querySelector('.box');
console.log(firstBox);
var nav = document.querySelector('#nav');
console.log(nav);
var li = document.querySelector('li');
console.log(li);
// 3. querySelectorAll()返回指定选择器的所有元素对象集合
var allBox = document.querySelectorAll('.box');
console.log(allBox);
var lis = document.querySelectorAll('li');
console.log(lis);
</script>
</body>
</html>
26-1-4 获取特殊元素(body,html)
4-1 获取body元素
js
doucumnet.body // 返回body元素对象
4-2 获取html元素
js
document.documentElement // 返回html元素对象
26-2 事件基础
26-2-1 事件的三要素
1. 事件源 (谁)
2. 事件类型 (什么事件)
3. 事件处理程序 (做啥)
26-2-2 案例:点击按钮弹出警示框
js
// 获取事件源(按钮)
// 注册事件(绑定事件),使用 onclick
// 编写事件处理程序,写一个函数弹出 alert 警示框
代码实现
JS
var btn = document.getElementById('btn');
btn.onclick = function() {
alert('你好吗');
};
26-2-3 执行事件的步骤
js
1,// 获取事件源
2. // 注册事件(绑定事件)
3. // 添加事件处理程序(采取函数赋值形式)
26-2-4 常见的鼠标事件
*鼠标事件* | *触发条件* |
---|---|
onclick | 鼠标点击左键触发 |
onmouseover | 鼠标经过触发 |
onmouseout | 鼠标离开触发 |
onfocus | 获得鼠标焦点触发 |
onblur | 失去鼠标焦点触发 |
onmousemove | 鼠标移动触发 |
onmouseup | 鼠标弹起触发 |
onmousedown | 鼠标按下触发 |
26-3 操作元素
26-3-1 改变元素的内容
js
element.innerText
// 从起始位置到终止位置的内容, 但它去除 html 标签, 同时空格和换行也会去掉
element.innerHTML
// 起始位置到终止位置的全部内容,包括 html 标签,同时保留空格和换行
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div></div>
<p>
我是文字
<span>123</span>
</p>
<script>
// innerText 和 innerHTML的区别
// 1. innerText 不识别html标签 非标准 去除空格和换行
var div = document.querySelector('div');
// div.innerText = '<strong>今天是:</strong> 2019';
// 2. innerHTML 识别html标签 W3C标准 保留空格和换行的
div.innerHTML = '<strong>今天是:</strong> 2019';
// 这两个属性是可读写的 可以获取元素里面的内容
var p = document.querySelector('p');
console.log(p.innerText);
console.log(p.innerHTML);
</script>
</body>
</html>
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
div,
p {
width: 300px;
height: 30px;
line-height: 30px;
color: #fff;
background-color: pink;
}
</style>
</head>
<body>
<button>显示当前系统时间</button>
<div>某个时间</div>
<p>1123</p>
<script>
// 当我们点击了按钮, div里面的文字会发生变化
// 1. 获取元素
var btn = document.querySelector('button');
var div = document.querySelector('div');
// 2.注册事件
btn.onclick = function() {
// div.innerText = '2019-6-6';
div.innerHTML = getDate();
}
function getDate() {
var date = new Date();
// 我们写一个 2019年 5月 1日 星期三
var year = date.getFullYear();
var month = date.getMonth() + 1;
var dates = date.getDate();
var arr = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
var day = date.getDay();
return '今天是:' + year + '年' + month + '月' + dates + '日 ' + arr[day];
}
// 我们元素可以不用添加事件
var p = document.querySelector('p');
p.innerHTML = getDate();
</script>
</body>
</html>
26-3-2 常用元素的属性操作
JS
1. innerText、innerHTML 改变元素内容
2. src、href
3. id、alt、title
演示
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
img {
width: 300px;
}
</style>
</head>
<body>
<button id="ldh">刘德华</button>
<button id="zxy">张学友</button> <br>
<img src="images/ldh.jpg" alt="" title="刘德华">
<script>
// 修改元素属性 src
// 1. 获取元素
var ldh = document.getElementById('ldh');
var zxy = document.getElementById('zxy');
var img = document.querySelector('img');
// 2. 注册事件 处理程序
zxy.onclick = function() {
img.src = 'images/zxy.jpg';
img.title = '张学友思密达';
}
ldh.onclick = function() {
img.src = 'images/ldh.jpg';
img.title = '刘德华';
}
</script>
</body>
</html>
2-1 分时间显示不同图片案例
根据不同时间,页面显示不同图片,同时显示不同的问候语。
如果上午时间打开页面,显示上午好,显示上午的图片。
如果下午时间打开页面,显示下午好,显示下午的图片。
如果晚上时间打开页面,显示晚上好,显示晚上的图片。
js
// 根据系统不同时间来判断,所以需要用到日期内置对象
// 利用多分支语句来设置不同的图片
// 需要一个图片,并且根据时间修改图片,就需要用到操作元素src属性
// 需要一个div元素,显示不同问候语,修改元素内容即可
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
img {
width: 300px;
}
</style>
</head>
<body>
<img src="images/s.gif" alt="">
<div>上午好</div>
<script>
// 根据系统不同时间来判断,所以需要用到日期内置对象
// 利用多分支语句来设置不同的图片
// 需要一个图片,并且根据时间修改图片,就需要用到操作元素src属性
// 需要一个div元素,显示不同问候语,修改元素内容即可
// 1.获取元素
var img = document.querySelector('img');
var div = document.querySelector('div');
// 2. 得到当前的小时数
var date = new Date();
var h = date.getHours();
// 3. 判断小时数改变图片和文字信息
if (h < 12) {
img.src = 'images/s.gif';
div.innerHTML = '亲,上午好,好好写代码';
} else if (h < 18) {
img.src = 'images/x.gif';
div.innerHTML = '亲,下午好,好好写代码';
} else {
img.src = 'images/w.gif';
div.innerHTML = '亲,晚上好,好好写代码';
}
</script>
</body>
</html>
26-3-3 表单属性设置
type、value、checked、selected、disabled
3-1 案例:仿京东显示密码
js
核心思路: // 点击眼睛按钮,把密码框类型改为文本框就可以看见里面的密码
一个按钮两个状态,点击一次,切换为文本框,继续点击一次切换为密码框
算法:// 利用一个flag变量,来判断flag的值,如果是1 就切换为文本框,flag 设置为0,如果是0 就切换为密码框,flag设置为1
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.box {
position: relative;
width: 400px;
border-bottom: 1px solid #ccc;
margin: 100px auto;
}
.box input {
width: 370px;
height: 30px;
border: 0;
outline: none;
}
.box img {
position: absolute;
top: 2px;
right: 2px;
width: 24px;
}
</style>
</head>
<body>
<div class="box">
<label for="">
<img src="images/close.png" alt="" id="eye">
</label>
<input type="password" name="" id="pwd">
</div>
<script>
// 1. 获取元素
var eye = document.getElementById('eye');
var pwd = document.getElementById('pwd');
// 2. 注册事件 处理程序
var flag = 0;
eye.onclick = function() {
// 点击一次之后, flag 一定要变化
if (flag == 0) {
pwd.type = 'text';
eye.src = 'images/open.png';
flag = 1; // 赋值操作
} else {
pwd.type = 'password';
eye.src = 'images/close.png';
flag = 0;
}
}
</script>
</body>
</html>
26-3-4 样式属性操作
我们可以通过 JS 修改元素的大小、颜色、位置等样式。
js
element.style // 行内样式操作
element.className // 类名样式操作
js
注意:
1.// JS 里面的样式采取驼峰命名法 比如 fontSize、 backgroundColor
2.// JS 修改 style 样式操作,产生的是行内样式,CSS 权重比较高
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
div {
width: 200px;
height: 200px;
background-color: pink;
}
</style>
</head>
<body>
<div></div>
<script>
// 1. 获取元素
var div = document.querySelector('div');
// 2. 注册事件 处理程序
div.onclick = function() {
// div.style里面的属性 采取驼峰命名法
this.style.backgroundColor = 'purple';
this.style.width = '250px';
}
</script>
</body>
</html>
4-1 案例: 淘宝点击关闭二维码
核心思路: 利用样式的显示和隐藏完成, display:none 隐藏元素 display:block 显示元素
点击按钮,就让这个二维码盒子隐藏起来即可
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.box {
position: relative;
width: 74px;
height: 88px;
border: 1px solid #ccc;
margin: 100px auto;
font-size: 12px;
text-align: center;
color: #f40;
/* display: block; */
}
.box img {
width: 60px;
margin-top: 5px;
}
.close-btn {
position: absolute;
top: -1px;
left: -16px;
width: 14px;
height: 14px;
border: 1px solid #ccc;
line-height: 14px;
font-family: Arial, Helvetica, sans-serif;
cursor: pointer;
}
</style>
</head>
<body>
<div class="box">
淘宝二维码
<img src="images/tao.png" alt="">
<i class="close-btn">×</i>
</div>
<script>
// 1. 获取元素
var btn = document.querySelector('.close-btn');
var box = document.querySelector('.box');
// 2.注册事件 程序处理
btn.onclick = function() {
box.style.display = 'none';
}
</script>
</body>
</html>
4-2 案例: 循环精灵图背景
js
// 首先精灵图图片排列有规律的
// 核心思路: 利用for循环 修改精灵图片的 背景位置 background-position
// 剩下的就是考验你的数学功底了
// 让循环里面的 i 索引号 * 44 就是每个图片的y坐标
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
li {
list-style-type: none;
}
.box {
width: 250px;
margin: 100px auto;
}
.box li {
float: left;
width: 24px;
height: 24px;
background-color: pink;
margin: 15px;
background: url(images/sprite.png) no-repeat;
}
</style>
</head>
<body>
<div class="box">
<ul>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
<script>
// 1. 获取元素 所有的小li
var lis = document.querySelectorAll('li');
for (var i = 0; i < lis.length; i++) {
// 让索引号 乘以 44 就是每个li 的背景y坐标 index就是我们的y坐标
var index = i * 44;
lis[i].style.backgroundPosition = '0 -' + index + 'px';
}
</script>
</body>
</html>
4-3 案例:显示隐藏文本框内容
js
// 首先表单需要2个新事件,获得焦点 onfocus 失去焦点 onblur
// 如果获得焦点, 判断表单里面内容是否为默认文字,如果是默认文字,就清空表单内容
// 如果失去焦点, 判断表单内容是否为空,如果为空,则表单内容改为默认文字
我们可以通过 JS 修改元素的大小、颜色、位置等样式。
js
element.style // 行内样式操作
element.className // 类名样式操作
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
input {
color: #999;
}
</style>
</head>
<body>
<input type="text" value="手机">
<script>
// 1.获取元素
var text = document.querySelector('input');
// 2.注册事件 获得焦点事件 onfocus
text.onfocus = function() {
// console.log('得到了焦点');
if (this.value === '手机') {
this.value = '';
}
// 获得焦点需要把文本框里面的文字颜色变黑
this.style.color = '#333';
}
// 3. 注册事件 失去焦点事件 onblur
text.onblur = function() {
// console.log('失去了焦点');
if (this.value === '') {
this.value = '手机';
}
// 失去焦点需要把文本框里面的文字颜色变浅色
this.style.color = '#999';
}
</script>
</body>
</html>
4-4 案例: 密码框格式提示错误信息
js
// 首先判断的事件是表单失去焦点 onblur
// 如果输入正确则提示正确的信息颜色为绿色小图标变化
// 如果输入不是6到16位,则提示错误信息颜色为红色 小图标变化
// 因为里面变化样式较多,我们采取className修改样式
26-3-5 操作元素总结
26-3-6 排他思想
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<button>按钮1</button>
<button>按钮2</button>
<button>按钮3</button>
<button>按钮4</button>
<button>按钮5</button>
<script>
// 1. 获取所有按钮元素
var btns = document.getElementsByTagName('button');
// btns得到的是伪数组 里面的每一个元素 btns[i]
for (var i = 0; i < btns.length; i++) {
btns[i].onclick = function() {
// (1) 我们先把所有的按钮背景颜色去掉 干掉所有人
for (var i = 0; i < btns.length; i++) {
btns[i].style.backgroundColor = '';
}
// (2) 然后才让当前的元素背景颜色为pink 留下我自己
this.style.backgroundColor = 'pink';
}
}
//2. 首先先排除其他人,然后才设置自己的样式 这种排除其他人的思想我们成为排他思想
</script>
</body>
</html>
6-1 案例:百度换肤
这个案例练习的是给一组元素注册事件
给4个小图片利用循环注册点击事件
当我们点击了这个图片,让我们页面背景改为当前的图片
核心算法: 把当前图片的src 路径取过来,给 body 做为背景即可
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
body {
background: url(images/1.jpg) no-repeat center top;
}
li {
list-style: none;
}
.baidu {
overflow: hidden;
margin: 100px auto;
background-color: #fff;
width: 410px;
padding-top: 3px;
}
.baidu li {
float: left;
margin: 0 1px;
cursor: pointer;
}
.baidu img {
width: 100px;
}
</style>
</head>
<body>
<ul class="baidu">
<li><img src="images/1.jpg"></li>
<li><img src="images/2.jpg"></li>
<li><img src="images/3.jpg"></li>
<li><img src="images/4.jpg"></li>
</ul>
<script>
// 1. 获取元素
var imgs = document.querySelector('.baidu').querySelectorAll('img');
// console.log(imgs);
// 2. 循环注册事件
for (var i = 0; i < imgs.length; i++) {
imgs[i].onclick = function() {
// this.src 就是我们点击图片的路径 images/2.jpg
// console.log(this.src);
// 把这个路径 this.src 给body 就可以了
document.body.style.backgroundImage = 'url(' + this.src + ')';
}
}
</script>
</body>
</html>
6-2 案例:表格隔行变色
用到新的鼠标事件 鼠标经过 onmouseover 鼠标离开 onmouseout
核心思路:鼠标经过 tr 行,当前的行变背景颜色, 鼠标离开去掉当前的背景颜色
注意: 第一行(thead里面的行)不需要变换颜色,因此我们获取的是 tbody 里面的行
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
table {
width: 800px;
margin: 100px auto;
text-align: center;
border-collapse: collapse;
font-size: 14px;
}
thead tr {
height: 30px;
background-color: skyblue;
}
tbody tr {
height: 30px;
}
tbody td {
border-bottom: 1px solid #d7d7d7;
font-size: 12px;
color: blue;
}
.bg {
background-color: pink;
}
</style>
</head>
<body>
<table>
<thead>
<tr>
<th>代码</th>
<th>名称</th>
<th>最新公布净值</th>
<th>累计净值</th>
<th>前单位净值</th>
<th>净值增长率</th>
</tr>
</thead>
<tbody>
<tr>
<td>003526</td>
<td>农银金穗3个月定期开放债券</td>
<td>1.075</td>
<td>1.079</td>
<td>1.074</td>
<td>+0.047%</td>
</tr>
<tr>
<td>003526</td>
<td>农银金穗3个月定期开放债券</td>
<td>1.075</td>
<td>1.079</td>
<td>1.074</td>
<td>+0.047%</td>
</tr>
<tr>
<td>003526</td>
<td>农银金穗3个月定期开放债券</td>
<td>1.075</td>
<td>1.079</td>
<td>1.074</td>
<td>+0.047%</td>
</tr>
<tr>
<td>003526</td>
<td>农银金穗3个月定期开放债券</td>
<td>1.075</td>
<td>1.079</td>
<td>1.074</td>
<td>+0.047%</td>
</tr>
<tr>
<td>003526</td>
<td>农银金穗3个月定期开放债券</td>
<td>1.075</td>
<td>1.079</td>
<td>1.074</td>
<td>+0.047%</td>
</tr>
<tr>
<td>003526</td>
<td>农银金穗3个月定期开放债券</td>
<td>1.075</td>
<td>1.079</td>
<td>1.074</td>
<td>+0.047%</td>
</tr>
</tbody>
</table>
<script>
// 1.获取元素 获取的是 tbody 里面所有的行
var trs = document.querySelector('tbody').querySelectorAll('tr');
// 2. 利用循环绑定注册事件
for (var i = 0; i < trs.length; i++) {
// 3. 鼠标经过事件 onmouseover
trs[i].onmouseover = function() {
// console.log(11);
this.className = 'bg';
}
// 4. 鼠标离开事件 onmouseout
trs[i].onmouseout = function() {
this.className = '';
}
}
</script>
</body>
</html>
6-3 案例:表单全选取消全选案例
- 全选和取消全选做法: 让下面所有复选框的checked属性(选中状态) 跟随 全选按钮即可
- 下面复选框需要全部选中, 上面全选才能选中做法: 给下面所有复选框绑定点击事件,每次点击,都要循环查看下面所有的复选框是否有没选中的,如果有一个没选中的, 上面全选就不选中。
- 可以设置一个变量,来控制全选是否选中
html
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<style>
* {
padding: 0;
margin: 0;
}
.wrap {
width: 300px;
margin: 100px auto 0;
}
table {
border-collapse: collapse;
border-spacing: 0;
border: 1px solid #c0c0c0;
width: 300px;
}
th,
td {
border: 1px solid #d0d0d0;
color: #404060;
padding: 10px;
}
th {
background-color: #09c;
font: bold 16px "微软雅黑";
color: #fff;
}
td {
font: 14px "微软雅黑";
}
tbody tr {
background-color: #f0f0f0;
}
tbody tr:hover {
cursor: pointer;
background-color: #fafafa;
}
</style>
</head>
<body>
<div class="wrap">
<table>
<thead>
<tr>
<th>
<input type="checkbox" id="j_cbAll" />
</th>
<th>商品</th>
<th>价钱</th>
</tr>
</thead>
<tbody id="j_tb">
<tr>
<td>
<input type="checkbox" />
</td>
<td>iPhone8</td>
<td>8000</td>
</tr>
<tr>
<td>
<input type="checkbox" />
</td>
<td>iPad Pro</td>
<td>5000</td>
</tr>
<tr>
<td>
<input type="checkbox" />
</td>
<td>iPad Air</td>
<td>2000</td>
</tr>
<tr>
<td>
<input type="checkbox" />
</td>
<td>Apple Watch</td>
<td>2000</td>
</tr>
</tbody>
</table>
</div>
<script>
// 1. 全选和取消全选做法: 让下面所有复选框的checked属性(选中状态) 跟随 全选按钮即可
// 获取元素
var j_cbAll = document.getElementById('j_cbAll'); // 全选按钮
var j_tbs = document.getElementById('j_tb').getElementsByTagName('input'); // 下面所有的复选框
// 注册事件
j_cbAll.onclick = function() {
// this.checked 它可以得到当前复选框的选中状态如果是true 就是选中,如果是false 就是未选中
console.log(this.checked);
for (var i = 0; i < j_tbs.length; i++) {
j_tbs[i].checked = this.checked;
}
}
// 2. 下面复选框需要全部选中, 上面全选才能选中做法: 给下面所有复选框绑定点击事件,每次点击,都要循环查看下面所有的复选框是否有没选中的,如果有一个没选中的, 上面全选就不选中。
for (var i = 0; i < j_tbs.length; i++) {
j_tbs[i].onclick = function() {
// flag 控制全选按钮是否选中
var flag = true;
// 每次点击下面的复选框都要循环检查者4个小按钮是否全被选中
for (var i = 0; i < j_tbs.length; i++) {
if (!j_tbs[i].checked) {
flag = false;
break; // 退出for循环 这样可以提高执行效率 因为只要有一个没有选中,剩下的就无需循环判断了
}
}
j_cbAll.checked = flag;
}
}
</script>
</body>
</html>
26-3-7 自定义属性
7-1 获取属性值
JS
element.属性 // 获取属性值。
element.getAttribute('属性');
区别:
// element.属性 获取内置属性值(元素本身自带的属性)
// element.getAttribute('属性'); 主要获得自定义的属性 (标准) 我们程序员自定义的属性
7-2 设置属性值
js
element.属性 = '值' // 设置内置属性值。
element.setAttribute('属性', '值');
区别:
// element.属性 设置内置属性值
// element.setAttribute('属性'); 主要设置自定义的属性 (标准)
7-3 移除属性
js
element.removeAttribute('属性');
自定义属性演示
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="demo" index="1" class="nav"></div>
<script>
var div = document.querySelector('div');
// 1. 获取元素的属性值
// (1) element.属性
console.log(div.id);
//(2) element.getAttribute('属性') get得到获取 attribute 属性的意思 我们程序员自己添加的属性我们称为自定义属性 index
console.log(div.getAttribute('id'));
console.log(div.getAttribute('index'));
// 2. 设置元素属性值
// (1) element.属性= '值'
div.id = 'test';
div.className = 'navs';
// (2) element.setAttribute('属性', '值'); 主要针对于自定义属性
div.setAttribute('index', 2);
div.setAttribute('class', 'footer'); // class 特殊 这里面写的就是class 不是className
// 3 移除属性 removeAttribute(属性)
div.removeAttribute('index');
</script>
</body>
</html>
7-4 案例:tab 栏切换(重点案例)
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
li {
list-style-type: none;
}
.tab {
width: 978px;
margin: 100px auto;
}
.tab_list {
height: 39px;
border: 1px solid #ccc;
background-color: #f1f1f1;
}
.tab_list li {
float: left;
height: 39px;
line-height: 39px;
padding: 0 20px;
text-align: center;
cursor: pointer;
}
.tab_list .current {
background-color: #c81623;
color: #fff;
}
.item_info {
padding: 20px 0 0 20px;
}
.item {
display: none;
}
</style>
</head>
<body>
<div class="tab">
<div class="tab_list">
<ul>
<li class="current">商品介绍</li>
<li>规格与包装</li>
<li>售后保障</li>
<li>商品评价(50000)</li>
<li>手机社区</li>
</ul>
</div>
<div class="tab_con">
<div class="item" style="display: block;">
商品介绍模块内容
</div>
<div class="item">
规格与包装模块内容
</div>
<div class="item">
售后保障模块内容
</div>
<div class="item">
商品评价(50000)模块内容
</div>
<div class="item">
手机社区模块内容
</div>
</div>
</div>
<script>
// 获取元素
var tab_list = document.querySelector('.tab_list');
var lis = tab_list.querySelectorAll('li');
var items = document.querySelectorAll('.item');
// for循环绑定点击事件
for (var i = 0; i < lis.length; i++) {
// 开始给5个小li 设置索引号
lis[i].setAttribute('index', i);
lis[i].onclick = function() {
// 1. 上的模块选项卡,点击某一个,当前这一个底色会是红色,其余不变(排他思想) 修改类名的方式
// 干掉所有人 其余的li清除 class 这个类
for (var i = 0; i < lis.length; i++) {
lis[i].className = '';
}
// 留下我自己
this.className = 'current';
// 2. 下面的显示内容模块
var index = this.getAttribute('index');
console.log(index);
// 干掉所有人 让其余的item 这些div 隐藏
for (var i = 0; i < items.length; i++) {
items[i].style.display = 'none';
}
// 留下我自己 让对应的item 显示出来
items[index].style.display = 'block';
}
}
</script>
</body>
</html>