JavaScript 高级程序设计(book 3rd edition)

  • <script>元素位置

一般惯例是在<head>元素中包含所有的<script>元素,但是这就意味着必须等到全部的javascript代码都被下载、解析和执行完成后,才开始呈现页面部分:

<!DOCTYPE html>
<html>
<head>
<script></script>
</head>
<body></body>
</html>

为了避免这个问题,现代Web应用程序一般把全部<scripy>元素放在<body>内,且放在页面的内容后面:

<!DOCTYPE html>
<html>
<head></head>
<body>
//
<script></script>
</body>
</html>

但是也会看到把 <script>元素放在 </body>标签之后 </html> 标签之前的。这么写的愿景是页面内容加载完再加载js?
按照HTML5标准中的HTML语法规则,如果在</body>后再出现<script>或任何元素的开始标签,都是parse error,浏览器会忽略之前的</body>,即视作仍旧在body内。所以实际效果和写在</body>之前是没有区别的。总之,这种写法虽然也能work,但是并没有带来任何额外好处,实际上出现这样的写法很可能是误解了“将script放在页面最末端”的教条。所以还是不要这样写为好。虽然将<script>写在</body>之后,但最终的DOM树里,<script>元素还是会成为body的子节点,这一点很容易在firebug等调试器里验证。

  • 延迟脚本

<script>元素的defer属性。
这个属性的用途是标明脚本在执行时不会影响页面的构造,也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,在<script>元素中设置defer属性,相当于告诉浏览器立即下载,但延迟执行。

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" defer="defer" scr="path/jsname.js"></script>
</head>
<body></body>
</html>

defer属性只适用于外部脚本文件,会忽略嵌入脚本设置的defer属性。

  • 异步脚本

html5为<script>元素定义了async属性。与defer类似的是,都用于改变处理脚本的行为,且也只适用于外部脚本。

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" async="async" scr="path/jsname1.js"></script>
<script type="text/javascript" async="async" scr="path/jsname2.js"></script>
</head>
<body></body>
</html>

上面代码中,第二个脚本可能会先于第一个脚本执行,因此,使用了async就要确保脚本间互不依赖。
指定async属性的目的是不让页面等待两个脚本下载和执行,从而异步加载页面其他内容,所以也建议不要在加载期间修改DOM。
异步脚本一定会在页面的load事件前执行,但可能会在DOMContentLoad事件触发之前或之后执行。

  • 重排序方法

两个可以直接使用的重排序方法:

  • reverse():反转数组项的顺序;
  • sort():默认情况下按升序排列数组项。但是为了实现排序,sort()方法会调用每个数组项的toString()方法,然后比较得到的字符串。所以,即使数组中的每一项都是数值,该方法比较的也是字符串。
var values=[0, 1, 5, 10, 15];
values.sort();
console.log(values);   //0, 1, 10, 15, 5

所以sort()方法可以接收一个比较函数作为参数,一边自定义比较方法。该标胶函数要接收两个参数,如果要求第一个参数应该位于第二个参数之前则返回一个负数,如果两个参数相等则返回0,如果第一个参数应该位于第二个参数之后则返回一个正数。

function compare(value1, value2){
  if(value1 < value2) return -1;
  else if(value1 > value2) return 1;
  else return 0;
}
var values=[0, 1, 5, 10, 15];
values.sort(compare);
console.log(values);    //0, 1 , 5, 10, 15
//所以compare也可以这样实现:
function compare(value1, value2){
  return value1-value2;
}
  • 基本包装

为了便于操作基本数据类型,ECMAScript提供了3个特殊的引用类型:Boolean,Number,String。每当读取一个基本类型值的时候,后台就会创建一个对应的基本包装类型的对象,从而能够让我们调用一些方法来对这些基本数据进行操作。

var s1 = "some example";
var s2 = s1.substring(2);

上例中s1是一个字符串基本类型,但是第二行却调用了一个方法,我们知道基本类型值不是对象,逻辑上是不该有自己的方法的。其实,系统为了实现该操作,在后台自动完成了一系列的处理。在读取模式中访问字符串时,后台都会自动完成下列处理:

  • 创建String类型的一个实例;
  • 在实例上调用指定的方法;
  • 销毁这个实例。
    所以,上面的两行实际是这样执行的:
var s1="some example";
var s3=new String(s1);
var s2=s3.substring(2);
s3=null;
console.log(s2);   //me example

经过此番处理,基本的字符串值就变得跟对象一样了。而且,上面三个步骤也适用于Boolean和Number。

引用类型与基本包装类型的主要区别就是对象的生存期。使用new操作符创建的引用类型的实例,在执行流离开当前作用域之前都一直保存在内存中。而自动创建的基本包装类型的对象,则只存在于一行代码的执行瞬间,而后立即被销毁,这也意味着我们不能在运行时为基本类型值添加属性和方法。

var s1 = "some example";
s1.color = "red";
console.log(s1.color);        //undefined
console.log(s1);     some example
var ss=new String("some example");
ss.color="red";
console.log(ss);   //String {0: "s", 1: "o", 2: "m", 3: "e", 4: " ", 5: "e", 6: "x", 7: "a", 8: "m", 9: "p", 10: "l", 11: "e", color: "red", length: 12, [[PrimitiveValue]]: "some example"}
  • Boolean

他们的建议是永远不要使用Boolean对象。

var falseObject = new Boolean(false);
var result = falseObject && true;
console.log(result); // true
//---
var falseValue = false;
var result = falseValue && true;
console.log(result); //false

出现上面这个差异的原因是:代码中是对falseObject而不是对它的值(false)进行求值。布尔表达式中的所有对象都会被转换为true,因此falseObject对象在布尔表达式中代表的是true而不是它的初值false。

  • Function

  • 函数内部属性

函数内两个特殊对象:argumentsthis
arguments的主要用途是保存函数参数,但是它还有一个重要属性callee,该属性是一个指针,指向拥有这个arguments对象的函数。

//例如阶乘函数
function factorial(num){
 if(num<=1) return 1;
else{return num*factorial(num-1);}
}
//使用arguments.callee
function factorial(num){
 if(num<=1) return 1;
else{return num*arguments.callee(num-1);}
}

上面的函数在函数有名字,且函数名字不变的情况下是没问题的。但是这个函数的执行与函数名factorial紧紧耦合在一起,为了消除这种耦合,使用arguments.callee。
////////////////////////////////////////////////////////
另一个函数对象的属性:caller。这个属性中保存着调用当前函数的函数的引用。如果是在全局作用域中调用当前函数,它的值为null。

function outer(){
      inner();
}
function inner(){
      console.log(inner.caller);
}
outer(); //输出outer的源代码,因为是outer调用的inner。所以为了去耦合也可以通过arguments.callee.caller来实现。
  • 函数属性和方法

每个函数都包含两个属性:length,prototype。
length表示函数希望接收的命名参数的个数。
prototype保存它们所有实例方法的真正所在。

  • apply() 与 call()

这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内的this对象的值。
apply()方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组,可以是Array的实例也可以是arguments对象。
call()与apply()的区别在参数不同:第一个也是this,而后面的参数是逐个列举传递。
事实上,apply()和call()最大的用武之地,真正强大的地方是能扩充函数赖以运行的作用域(类似C++的多态?):

window.color = "red";
var object = {"color":"blue"};
function sayColor(){
  alert(this.color);
}
sayColor();     //red
sayColor.call(this);   //red
sayColor.call(window);   //red
sayColor.call(object);    //blue

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

  • bind

这个方法会创建一个函数的实例,其this值会被绑定到传给bind()函数的值(类似C++多态预编译时多态?)

window.color = "red";
var object = {"color": "blue"};
function sayColor(){
  alert(this.color);
}
var objectSayColor=sayColor.bind(object);//方法绑定到对象,并返回一个函数实例
objectSayColor();  //blue
  • 创建对象

  • 工厂模式

工厂模式就是可批量生产喽,就是以函数的方式创建对象,用函数来封装特定接口创建对象的细节:

function createPerson(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
  alert(this.name);
};
return o;
}
var persion1 = createPerson("Greg",27,"Doctor");

但是该方式没有解决对象识别问题(怎样获得一个对象的类型)。

  • 构造函数模式

创建自定义的构造函数意味着可以将它的实例标识为一种特定的类型,而这正是构造函数模式胜过工厂模式的地方。

function Person(name,age,job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName= function(){
  alert(this.name);
  };
}
var person1 = new Person("Greg",27,"Doctor");

与工厂模式相似,但是明显不同。
创建的对象person1隐含一个constructor属性,该属性指向Person:

alert(person1.constructor == Person);//true

但是,检测对象类型还是instanceof操作符更可靠一些。

alert(person1 instanceof Object);//true
alert(person1 instanceof Person);//true
  • 构造函数与普通函数

两者的区别就是在调用是产生的,否则构造函数就是普通函数,普通函数也可作为构造函数调用:

//构造
var person = new Person("Greg",27,"Doctor");
person.sayName();//Greg
//普通函数调用
Person("Greg",27,"Doctor");
window.sayName();//Greg
//在另一个对象的作用域中调用
var o = new Object();
Person.call(o,"Greg",27,"Doctor");
o.sayName();//Greg
  • 原型模式

我们创建的每一个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象。
而这个对象的用途是用来包含所有实例共享的属性和方法。
使用原型对象的好处就是让所有对象实例共享它所包含的属性和方法。
同时,这也是它最大的缺点。

function Persion(){}
Person.prototype.name = "Greg";
Person.prototype.age = 27;
Person.prototype.sayName = function(){alert(this.name);};
var person1 = new Person();
person1.sayName();//Greg
var person2 = new Person();
person2.sayName();//Greg
alert(person1.sayName == person2.sayName);//true,共享所有属性和方法
理解原型对象

创建一个新函数,就会创建一个prototype属性,这个属性指向函数的原型对象。
默认情况下所有原型对象都会获得夜歌constructor属性,这个属性包含一个指向prototype属性所在函数的指针。
以前例来说:
Person.prototype.constructor指向Person。
当调用构造函数创建一个新实例,该实例内部将会包含一个指针,指向构造函数的原型对象,ECMA5管这个指针叫[[Prototype]],在浏览器控制台看到的是proto
要明确的一点是,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。

s.PNG

图中,展示了Person构造函数,Person的原型属性以及Person现有两个实例之间的关系。
Person.prototype指向原型对象,
Person.prototype.constructor指回Person,
原型对象中包含constructor及后来添加的其他属性,
Person的每一个实例都包含一个内部属性,该属性仅仅指向Person.prototype。

isPrototypeOf

确定对象实例是否来自某个对象原型
alert(Person.prototype.isPrototypeOf(person1));//true

Object.getPrototypeOf()

返回对象实例的[[Prototype]]值。
alert(Object.getPrototypeOf(person1) == Person.prototype);//true
alert(Object.getPrototypeOf(person1).name);//Greg

hasOwnPrototype()

这里涉及到读取对象属性时的搜索顺序,
搜索先从对象实例本身开始,如果实例中存在就返回该属性的值,
否则继续向上搜索指针指向的原型对象。
所以,如果我们在实例中重写覆盖了原型中的值,那么返回的就是新值,而非共享的属性值。

person1.name = "Nicholas";
alert(person1.name);//Nicholas   来自实例
alert(person2.name);//Greg   来自原型

所以,实例中重写的值只是屏蔽对原型中同名属性的访问,而不是修改原型中的那个属性。
如要改回,使用delete操作符就可以完全删除实例中的同名属性。
使用hasOwnProperty()方法可以检测一个属性是存在于实例中还是原型中,
这个方法只在给定属性存在于对象实例中时才会返回true。

alert(person2.hasOwnProperty("name"));//false   实例中不存在
alert(person1.hasOwnProperty("name"));//true   实例中存在
delete person1.name;
alert(person1.hasOwnProperty("name"));//false   实例中不存在

通过该方法就能确定什么时候访问的是示例属性,什么时候是原型属性。


s.PNG
原型与in操作符

in操作符会在能够通过对象访问给定属性时返回true,无论该属性存在于实例中还是原型中,

alert("name" in person1);//true

所以,结合hasOwnProperty()就能判读属性存在的位置。

Object.keys()

获取对象上所有可枚举的实力属性
参数为对象,返回一个包含所有可枚举属性的字符串数组。

var keys = Object.keys(Person.prototype);//["name","age","sayName"],因为constructor的Enumrable默认为false
person1.name = "Bob";
var keys1 = Object.keys(person1);//["name"]  只输出实例的
getOwnPrototypeNames()

返回所有实例属性,无论是否可枚举。

  • 更常用的原型语法

从原型prototype的定义可以知道,prototype下是一个对象。
所以:

function Person(){}
Person.prototype = {
    name :"Greg",
    age : 27,
    sayName : function(){alert(this.name);}
};

但是这样又出现一个问题,
这样定义后,constructor不再指向Person了,
因为这种定义方法本质上是对原prototype的完全重写(所以,对原型完全重写后的原型就是另一个原型了),而创建新的prototype对象,这个对象也会自动重新获得constructor,所以新的consytructor就不指回原Person了,

var friend = new Person();
alert(friend instanceof Object);//true
alert(friend instanceof Person);//true
alert(friend.constructor == Person );//false

解决方法,
如果constructor的值很重要,就重新指定回原值

Person.prototype = {
  constructor:Person,
  ........
  ........
}
  • 原型的动态性

重写整个原型,就会切断构造函数与最初原型之间的联系,这个上面有提到。

  • 原型对象的问题

开始有提到,原型中所有的属性是被实例共享的,
这种共享对函数很适合,
对于那些包含基本值的属性也可以,同名属性屏蔽,
但是对于引用类型值的属性,问题就突出了:

function Person(){}
Person.prototype = {
  name = "Bob",
  age = 27,
  friends: ["Sheldy", "Court"]
};
var person1 = new Person();
var person2 = new Person();
person1.friend.push("Vans");
alert(person1.friends);//Sheldy,Court,Vans
alert(person2.friends);//Sheldy,Court,Vans
alert(person1.friends == person2.friends);//true
  • 组合使用构造函数模式和原型模式

解决上面遇到的问题
同时,也是创建自定义类型的最常用方式。
构造函数模式来定义示例的非共享属性,
原型模式来定义方法和共享的属性。
结果,每个实例都会有自己的一份实例属性的副本,
同时又共享着对方法的引用,最大限度的节省了内存。
而且,这种混合模式还支持向构造函数传递参数。

function Person(name,age,friends){
  this.name = name;
  this.age = age;
  this,friends = friends;
}
Person.prototype = {
  constructor: Person,
  sayName:function(){alert(this.name);}
}
  • 继承

  • js只支持实现继承,而且主要是依靠原型链实现。
  • 原型链
  • 先跳过
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容