你根本不懂Javascript(EP1~EP3.5 基础课)
目录
做全栈许久, 因为负责的方面多因此各语言都没有掌握精髓。因此准备翻看一些真正权威并且全面的书进行查缺补漏,计划从前端开始玩,近日在读《Javascript权威指南》,发觉自己居然有如此之多的部分全然不知,不禁一句感叹“你根本不懂Javascript!”
原本这只是个人笔记,记录一些自己错过的基础知识。和某大牛交谈过程中,被推荐将笔记公开并创造与大众交流心得的可能性,故将此文发布了出来。
包装对象和原始值
ECMAScript 有 5 种原始类型(primitive type)
- Undefined
- Null
- Boolean
- Number
- String
基本类型(null, undefined, bool, number, string)应该是值类型,没有属性和方法。
内置对象
Javascript有一系列内置对象来创建语言的基本功能,具体有如下几种
Boolean
- Boolean 对象表示两个值:
true或false。 - 当作为一个构造函数(带有运算符 new)调用时,Boolean() 将把它的参数转换成一个布尔值,并且返回一个包含该值的 Boolean 对象。
- 如果作为一个函数(不带有运算符 new)调用时,Boolean() 只将把它的参数转换成一个原始的布尔值,并且返回这个值,如果省略 value 参数,或者设置为
0、-0、null、""、false、undefined或NaN,则该对象设置为 false。否则设置为 true(即使 value 参数是字符串false)。
Boolean 对象包括 toString 和 valueOf 方法, Boolean 最常用于在 条件语句中 true 或 false 值的简单判断,布尔值和条件语句的组合提供了一种使用 Javascript 创建逻辑的方式。
Number
Number对象是一个数值包装器,该对象包含几个只读属性:
- MAX_VALUE:1.7976931348623157e+308 //Javascript能够处理的最大数
- MIN_VALUE:5e-324 //Javascript能够处理的最小数
- NEGATIVE_INFINITY:-Infiny //负无穷
- POSITIVE_INFINITY:Infinity //正无穷
- NaN:NaN //非数字
Number 对象还有一些方法,可以用这些方法对数值进行格式化或进行转换:
- toExponential //以指数形式返回 数字的字符串表示
- toFixed //把Number四舍五入为指定小数位数的数字
- toPrecision //在对象的值超出指定位数时将其转换为指数计数法
- toString //返回数字的字符串表示
- valueOf //继承自object
String
String 对象是文本值的包装器。除了存储文本,String 对象包含一个属性和各种 方法来操作或收集有关文本的信息,String 对象不需要进行实例化便能够使用。
String 对象只有一个只读的length属性用于返回字符串的长度。
包装对象
除了上面三个对象,Javascript还拥有Date、Array、Math等内置对象,这三个经常显示使用,所以非常熟悉,知道了内置对象就可以看看上面例子是怎么回事儿了。
只要是引用了字符串的属性和方法,Javascript就会将字符串值通过new String(s)的方式转为内置对象String,一旦引用结束,这个对象就会销毁。所以上面代码在使用的实际上是String对象的length属性和indexOf方法。
同样的道理,数字和布尔值的处理也类似。null和undefined没有对应对象。
既然有对象生成,能不能这样:
var s='this is a string';
s.len=10; //创建了一个临时的String对象,随即销毁
alert(s.len); //第三行代码又会创建一个新的临时对象, 并没有返回10,而是undefined!
a = 1;
a.s = 2;
a.s// 一样undefined
- 第二行代码只是创建了一个临时的String对象,随即销毁。
- 第三行代码又会创建一个新的临时对象,自然没有len属性。
- 这个创建的临时对象就成为包装对象。
如何区分原始对象和包装对象
Javascript会在必要时将包装对象转换为原始值因此显示创建的对象和其对应的原始值常常但不总是表现的一样。
==运算符将原始值和其包装对象视为相等;- 但
===全等运算符将他们视为不等; - 另外通过typeof运算符可以看到原始值和包装对象的不同。
不可变的原始值和可变的对象引用
Javascript中的原始值(undefined、null、布尔值、数字和字符串)与对象(包括数组和函数)有着根本区别。
原始值是不可更改的:任何方法都无法更改(或突变)一个原始值。
对数字和布尔值来说显然如此——改变数字的值本身就说不通,而对字符串来说就不那么明显了,因为字符串看起来像由字符组成的数组,我们期望可以通过指定索引来假改字符串中的字符。
实际上,Javascript是禁止这样做的。字符串中所有的方法看上去返回了一个修改后的字符串,实际上返回的是一个新的字符串值。
//字符串原始值修改不了
var str = "abc";
str[0] = "d";
console.log(str[1]="f"); //>>f
console.log(str[0]); //>>a
console.log(str); //>>abc
原始值的比较是值的比较, 但是对象是引用类型, 因此可以看成是地址的比较
var a = {'x' : 1}, b = {'x' : 1};
alert(a === b);//false, 值相同但是地址不同
var c = [1], d = [1];
alert(c === d);//false, 同上
对象转换为原始值
- 对象转换为到布尔值比较简单,所有对象到布尔都是true,包括包装类new Boolean(false)是一个对象而不是原始值,它将转换为true
- 对象到数字,对象到字符串比较复杂一些。注意这里讨论的是本地对象,不包含宿主对象(例如浏览器定义的对象)
所有对象继承了以下两个转换方法:
toString()
它的作用是返回一个反映这个对象的字符串。默认的toString()方法并不会返回一个有趣的值。
很多类定义了特定版本的toString()方法:
-
数组的toString() 方法将每个数组元素转换为一个字符串,并在元素之间添加逗号合并成结果字符串
-
函数类的toString() 方法返回这个函数的实现定义的表示方式。通常是将用户定义的函数转换为Javascript源代码字符串
-
日期类toString() 返回一个可读的日期和时间字符串。
-
RegExp类的toString() 将返回RegExp对象转换为表示正则表达式直接量字符串。
[1,2,3].toString()//=>`1,2,3` (function(x){f(x);}).toString()//=>` function(x){\nf(x);\n}` /\d+/g.toString()//=>`/\\d+/g` newDate(2010,0,1).toString() //=>`Fri Jan 01 2010 00:00:00 GMT-0800(PST)`
valueOf()
对象是复合值,而且大多数对象无法真正表示一个原始值。数组、函数和正则表达式简单地继承了这个默认方法,调用这些类型的实例的valueOf()方法只是简单地返回对象本身。日期类的valueOf方法会返回一个内部表示:1970年1月1日以来的毫秒数
通常情况下对象是通过toString()和valueOf()方法,就可以做到对象到字符串和对象到数字的转换。
对象到字符串转换逻辑
- 如果具有toString()方法,则调用这个方法,如果它返回一个原始值,js将其转换为字符串,并返回这个字符结果。
- **如果没有toString()或者这个方法并不返回一个原始值,那么js会去调用valueOf()。**如果有调用它,如果返回值是原始值。则将其转换成字符串。
- 如果没有toString()或valueOf()获得一个原始值,因此会抛出一个类型错误异常。
逻辑很清晰,先试试
toString()能否获得正确的值,如果不行再试试valueOf(),否则报错。
对象到数值的转换
- 如果对象具有valueOf()方法,后者返回一个原始值,则Javascript 将这个原始值转换为数字并返回这个数字
- 否则,如果对象具有toString() 方法,后者返回一个原始值,则js将这个原始值转换返回
- 否则,js报类型错误。
以上是翻译的原文,可能有些难读,不过其实也很容易理解:先试试
valueOf()然后再试试toString(),否则报错。
运算符使用时的数值转换
- Javascript里面的
+运算符可以进行加法或者字符串连接操作。如果其中一个操作数是对象,那么就会将对象转为原始值而不是执行对象到数字的转换。 ==操作符类似,如果对象和一个原始值进行比较, 那个对象也会转换成一个原始值。另外,日期类型是一种特殊的情况,日期是Javascript语言核心中唯一的预先定义类型。**对于所有非日期对象,对象到原始值的转换基本上是对象到数字的转换(首先调用valueOf()),日期对象则使用对象到字符串的转换模式。**并且,通过valueOf()或者toString()返回的原始值将本地直接使用而不会被强制转换为数字或字符串。- 和
==一样,<运算符以及其它关系算术运算符也会做到对象到原始值得转换,但是如果是日期对象则会使用上方粗体字的特殊的逻辑。因此除了日期对象之外的任何对象比较都会先尝试调用valueOf, 然后调用toString。不管得到的原始值是否直接使用,它都不会进一步被转换为数字或字符串。 +、==、!=关系运算符是唯一执行特殊的字符串到原始值的转换方式的运算符。其它运算符到特定类型的转换很明确,而且对日期对象来讲也没有特殊情况。例如-运算符把它的两个操作数都转换为数字。
日期对象各种运算的结果:
var now=new Date();
console.log(typeof (now+1)); //string +号把日期转换为字符串
//对于加号操作符,我们会将操作对象转换为字符串然后进行计算
console.log(typeof (now-1)); //number -号把对象到数字的转换
//对于减号操作符,我们会将操作对象转换为数字然后进行计算
console.log(now==now.toString()); //true
//对于比较操作符,我们一般会优先转换为原始值再进行比较
//但是日期类型例外!和日期对象相比较会转换成字符串再进行比较
console.log(now>now-1);//true >把日期转换为数字
变量声明
- 变量未赋值前的初始值是
undefined,不是null,不是null,不是null! - 我们不会给变量声明类型, 因此将一个原本是数字的变量重新赋给字符串的值也是合法的,但是一般要避免这种情况出现。
重复的声明和遗漏的声明
- 使用var语句多次声明同一个变量不仅是合法的,而且也不会造成任何错误。
- 如果重复的声明有一个初始值,那么它担当的不过是一个赋值语句的角色。
- 如果尝试读一个未声明的变量的值,Javascript会生成一个错误。
- 如果尝试给一个未用var声明的变量赋值,Javascript会隐式声明该变量。
- 但是要注意,隐式声明的变量总是被创建为全局变量,即使该变量只在一个函数体内使用。局部变量是只在一个函数中使用,要防止在创建局部变量时创建全局变量(或采用已有的全局变量),就必须在函数体内部使用var语句。无论是全局变量还是局部变量,最好都使用var语句创建。
变量作用域
-
所有末定义直接赋值的变量自动声明为拥有全局作用域
-
一般情况下,
window对象的内置属性都拥有全局作用域,例如window.name、window.location、window.top等等。 -
尽管在全局作用域编写代码时可以不写
var语句,但声明局部变量时则必须使用var语句。scope = "global"; // 声明一个全局变量,甚至不用 var 来声明 function checkscope2() { scope = "local"; // 糟糕!我们刚修改了全局变量 myscope = "local"; // 这里显式地声明了一个新的全局变量 return [scope, myscope];// 返回两个值 } console.log(checkscope2()); // ["local", "local"],产生了副作用 console.log(scope); // "local",全局变量修改了 console.log(myscope); // "local",全局命名空间搞乱了 -
函数定义是可以嵌套的。
var scope = "global scope"; // 全局变量 function checkscope() { var scope = "local scope"; //局部变量 function nested() { var scope = "nested scope"; // 嵌套作用域内的局部变量 return scope; // 返回当前作用域内的值 } return nested(); } console.log(checkscope()); // "nested scope"
函数作用域和声明提前
**在一些类似 C 语言的编程语言中,花括号内的每一段代码都具有各自的作用域,而且变量在声明它们的代码段之外是不可见的,我们称为块级作用域(block scope),而 Javascript 中没有块级作用域。**Javascript 取而代之地使用了函数作用域(function scope),变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的。
在如下所示的代码中,在不同位置定义了变量 i、j 和 k,它们都在同一个作用域内,这三个变量在函数体内均是有定义的。
function test(o) {
var i = 0; // i在整个函数体内均是有定义的
if (typeof o == "object") {
var j = 0; // j在函数体内是有定义的,不仅仅是在这个代码段内
for (var k = 0; k < 10; k++) { // k在函数体内是有定义的,不仅仅是在循环内
console.log(k); // 输出数字0~9
}
console.log(k); // k已经定义了,输出10
}
console.log(j); // j已经定义了,但可能没有初始化
}
**Javascript 的函数作用域是指在函数内声明的所有变量在函数体内始终是可见的。**有意思的是,这意味着变量在声明之前甚至已经可用。Javascript 的这个特性被非正式地称为声明提前(hoisting),即 Javascript 函数里声明的所有变量(但不涉及赋值)都被「提前」至函数体的顶部,看一下如下代码:
var scope = "global";
function f() {
console.log(scope); // 输出"undefined",而不是"global"
//因为在这个作用域里面局部变量已经覆盖了全局变量,但是还没有执行到
var scope = "local"; // 变量在这里赋初始值,但变量本身在函数体内任何地方均是有定义的
console.log(scope); // 输出"local"
}
你可能会误以为函数中的第一行会输出 "global",因为代码还没有执行到 var 语句声明局部变量的地方。**其实不然,由于函数作用域的特性,局部变量在整个函数体始终是有定义的,也就是说,在函数体内局部变量遮盖了同名全局变量。**尽管如此,**只有在程序执行到 var 语句的时候,局部变量才会被真正赋值。**因此,上述过程等价于:将函数内的变量声明“提前”至函数体顶部,同时变量初始化留在原来的位置:
function f() { var scope; // 在函数顶部声明了局部变量 console.log(scope); // 变量存在,但其值是"undefined" scope = "local"; // 这里将其初始化并赋值 console.log(scope); // 这里它具有了我们所期望的值 }
在具有块级作用域的编程语言中,在狭小的作用域里让变量声明和使用变量的代码尽可能靠近彼此,通常来讲,这是一个非常不错的编程习惯。由于 Javascript 没有块级作用域,因此一些程序员特意将变量声明放在函数体顶部,而不是将声明靠近放在使用变量之处。这种做法使得他们的源代码非常清晰地反映了真实的变量作用域。
作为属性的变量
当声明一个Javascript全局变量时,实际上是定义了全局对象的一个属性。
**当使用var声明一个变量时,创建的这个属性是不可配置的,也就是说这个变量无法通过delete运算符来删除。**可能你已经注意到,如果你没有使用严格模式并给一个未声明的变量赋值的话,Javascript会自动创建一个全局变量。以这种方式创建的变量是全局对象的正常可本会属性,并可以删除它们:
var a =1;
b =2;
this.b2 = 3;
delete a; //不可删除
delete b; //可删除
delete this.b2 //可删除
Javascript全局变量是全局对象的属性,这是在ECMAScript 5规范称为“声明上下文对象。Javascript可以允许用this关键字来引用全局对象,却没有方法可以引用局部变量中存放的对象。这种存放局部变量的对象的特有性质,是一种对我们不可见的内部实现。然而,这些局部变量对象存在的观念是非常重要的。
表达式和运算符
原始表达式
原始表达式分为三种:
- 直接量
1.23 //数字直接量 "hello" //字符串直接量 /pattern/ //正则表达式直接量 - 保留字
true //返回一个布尔值:真 false //返回一个布尔值:假 null //返回一个值:空 this //返回“当前”对象 - 变量
i //返回变量i的值 sum //返回sum的值 undefined //undefined是全局变量,和null不同,它不是一个关键字
对象和数组的初始化表达式
对象和数组初始化表达式实际上是一个创建新的对象和数组。
因为数组本身就是一个对象
[] //一个空数组:[]内留空即表示该数组没有任何元素
[1+2,3+4] //拥有两个元素的数组,第一个是3,第二个是7
var matrix =[[1,2,3],[4,5,6],[7,8,9]];//数组的初始化可以嵌套
var a = new Array(1,2,3)
a = [1,2,3]
var a = new Array(1,,2,3)//报错
a = [1,,2,3]//index=1的元素是undefined
var a = new Array(10)//创建一个长度为10的数组并且值全部为undefined
a = [10]//创建了一个长度为1的数组并且值为10
属性访问表达式
一个典型的函数定义表达式包含关键字function,跟随其后的是一对圆括号,括号内是一个以逗号分隔的列表,列表含有0个或多个标识符(参数名),然后再跟随一个由花括号包裹的JS代码段(函数体),如:
var o = {x:1,y:{z:3}}; //一个示例对象
var a = [o,4,[5,6]]; //一个包含这个对象的示例数组
o.x //=>1:表达式o的x属性
o.y.z //=>3:表达式o.y的z属性
o["x"] //=>1:对象o的x属性
a[1] //=>4:表达式a中索引为1的元素
a[2]["1"] //=>6:表达式a[2]中索引为1的元素
a[1+1][0+1] //=>6:表达式a[2]中索引为1的元素,大括号里的数据运算后并且转换为了字符串
a[0].x //1:表达式a[0]的x属性
相关运算逻辑:
- 不管用哪种形式的属性访问表达式,在”.”和“[”之前的表达式总是会首先计算。
- 如果计算结果是
null或者undefined,表达式会抛出一个类型错误异常,因为这两个值都不能包含任意属性。 - 如果运算结果不是对象(或者数组),JS会将其转换为对象。
- 如果对象表达式后跟随句点和标识符,则会查找有这个标识符所指定的属性的值,并将其作为整个表达式的值返回。
- 如果对象表达式后跟随一对方括号,则会计算方括号内的表达式的值并将它转换为字符串。(注意是计算方括号里面的表达式的值并且转换为字符串)
- 不论哪种情况,如果命名的属性不存在,那么整个属性访问表达式的值就是
undefined。
如果属性名称是一个保留字或者包含空格和标志点符号,或是一个数字(对于数组来说),则必须使用方括号的写法。 当属性名是通过运算得出的而不是固定值的时候,这时必须使用方括号写法。
调用表达式
调用表达式以一个函数表达式开始,这个函数表达式指代了要调用的函数。函数表达式后跟随一对圆括号,括号内是一个以逗号隔开的参数列表,如:
f(0) //f是一个函数表达式;0是一个参数表达式
Math.max(x,y,z) //Math.max是一个函数;x,y和z是参数
a.sort() //a.sort是一个函数,它没有参数
- 当对调用表达式进行求值的时候,先计算函数表达式,然后计算参数表达式,得到一组参数值。
- 如果函数表达式的值不是一个可调用的对象,则抛出一个类型错误异常。
- 然后实参的值被依次赋值给形参,这些形参是定义函数时指定的,接下来开始执行函数体。如果函数使用
return语句给出一个返回值,那么这个返回值就是整个调用表达式的值。否则,调用表达式的值就是undefined。 - 任何一个调用表达式都包含一对圆括号和左圆括号之前的表达式。
- 如果这个表达式是一个属性访问表达式,那么这个调用称作
方法调用。在方法调用中,执行函数体的时候,作为属性访问主体的对象和数组便是其调用方法内this的指向。
这部分this的描述很模糊,不过后面会有详细的介绍
对象创建表达式
对象创建表达式(object creation expression)创建一个对象并调用构造函数来初始化对象的属性。对象创建表达式和函数调用表达式非常类似,只是对象创建表达式之前多了一个关键字new:
new Object()
new Point(2,3)
如果对象创建表达式不需要传入任何参数给构造函数的话,那么这对圆括号是可以省略掉的
new Object
new Point
运算符概述
+运算符
- 如果一个操作数是对象:
则对象会遵循对象到原始值的转换规则为原始类值。
日期对象
toString()方法执行转换,其他对象如果valueOf()方法返回一个原始值的话,则通过valueOf()方法执行转换。 由于多数对象都不具备可用的valueOf()方法,因此他们会通过toString()方法来执行抓换 - 在进行了对象到原始值的转换后,如果其中一个操作鼠是字符串的话,另一个操作数也会转换为字符串。然后进行字符串连接。
- 否则,两个操作数都将转换为数字(或者NaN),然后进行加法操作。
总结归纳:
- 如果是日期那么就使用
toString()
- 如果不是日期那么看看
valueOf()能否返回一个原始值
- 如果可以,那么就使用这个原始值
- 如果不行或者当前对象的
valueOf()不可用,那么就使用toString()
- 以上运算之后
- 如果一个操作数是字符串,另一个操作数也会转为字符串
- 如果没有字符串参与运算,那么就将操作数转换为数字然后进行加法操作.这儿不合法的数字都会转成
NaN
1 + 2 //=>3 加法
"1" + "2" //=>"12" 字符串连接
"1" + 2 //=>"12"数字转换为字符串后进行字符串连接
1 + {} //=>"1[object object]":对象转换为字符串后进行字符串连接
true + true //=>2 布尔值转换为数字后做加法
2 + null //=>2 null转换为0后做加法
2 + undefined //=>NaN undefined转换为NaN做加法
因此,加法符号类型混用时需要注意其优先级:
1 + 2 + "bmice" //=> "3 bmice"
1 + (2 + "bmice") => "12bmice"
递增/递减运算符
先来看段代码
var a=1;
a++;//输出2
var a="1";
a++;//输出2,首先将1转换为数字然后自增
var a="abc";
a++;//输出Nan,因为abc无法转换为数字
"a"+1; //输出a1
"a"++; //Uncaught ReferenceError: Invalid left-hand side expression in postfix operation,因为左操作数无法转换为数字
- 递增“++”运算符对其操作数进行增量(+1)的操作,操作数是一个左值(变量、数组元素或者对象属性)。
- 运算符将操作数转换为数字。
- 然后给数字加1,并将加1后的数值重新赋值给变量,数组元素或者对象属性。
关系表达式
相等和不等运算符
==运算符用于检测两个操作数是否相等,这里的比较很宽松因为允许了类型转换,检测室会通过如下逻辑:
- 如果一个值是
null另一个是undefined,则相等 - 如果一个是数字一个是字符串,字符串转为数字再比较
- 如果是
true则转换成1,false转换成0 - 如果一个值是对象另一个是数字或字符串,对象则转换成原始值(参考上文逻辑,注意日期类的例外情况)
===的检测就比较严格,会通过如下逻辑:
- 如果两个值类型不同,则不相等
- 如果两个值都是
null/undefined,则不相等 - 如果两个值都是布尔值
true或者都是布尔值false,则相等 - 如果一个是
NaN或者都是NaN,则不相等(NaN与任何值都不相等) - 如果都是数字并且值相等,则相等
- 如果都是字符串并且对应16位值相同,则相等
- 如果两个引用值指向同一个对象,则相等
比较运算符
包含各种>,<,>=,<=等比较运算符的运算逻辑:
- 如果操作数为对象,转换成原始值
- 转换后如果都是字符串那么按照字母表顺序比较
- 转换后如果至少一个不是字符串,那么两个都转为数字进行比较
- 如果转换后一个值是
NaN那么一定返回false
typeof
typeof也是一个运算符!
delete
delete: 没想到吧, 我也是运算符~
-
删除属性或者删除数组元素不仅仅是设置一个
undefined的值,实际这个属性将不再存在。 -
读取一个不存在的属性将返回
undefined -
用户var语句声明的变量不能删除
-
通过function语句定义的函数和函数参数也不能被删除
var o={x:1,y:2}; delete o.x;//true typeof o.x;//undefined delete o.x;//true delete o;//false, var定义的变量无法删除 delete 1;//...闹哪样? this.x=1;//重新赋值,注意没有var delete x;//非严格模式下返回true //严格模式下会抛出异常,应该用delete this.x代替 x;//运行错误
对象
-
除了原始类型的
字符串、数字、布尔值、null、undefined之外,其他值都是对象。 -
对象是可变的,可以理解成除上述之外的值都是对对象的引用
var x = Obj(); var y = x; x===y // true
对象属性
- 属性包含名字和值,属性名可以是包含空字符串在内的任意字符串,当然不能同时存在两个同名属性
- 属性的值可以是任意值,或者可以是一个
getter或者setter函数 - 每一个属性还有一些与之相关的值,操作
属性特性(property attribute) - 可写(writable attr)
- 可枚举(enumerable attr):决定是否可以通过
for/in循环该属性 - 可配置(Configurable attr):表名是否可以删改此属性
对象属性的查询和设置
使用圆点.或者方括号[]进行属性访问
- 点运算符后面的标识符不能是保留字
- 方括号引用对象属性的时候,
括号内的表达式必须返回一个转换成字符串的值
删除属性
使用delete关键字就可以删除属性,无论删除的属性是否存在,只要删除成功就会返回true
delete book.author;
delete book["author name"];
- 可配置性为
false的属性无法删除 - 注意
delete只能删除自有属性,无法删除继承属性。(如果要删除继承属性只能从原型处进行操作)
属性检测
可以通过4种方法进行属性检测:
-
in运算符var o = {x:1}; "x" in o;//true "y" in o;//false "toString" in o;//true -
hasOwnPreperty() -
PropertyIsEnumerable()对象有对应属性并且可枚举性为
true才返回true内置属性是不能枚举的
-
通过
!==判断属性是否为undefinedo.x!==undefined//true o.y!==undefined//false
对象特性
- 对象的
原型(prototype)指向另一个对象,本对象的属性都继承于原型对象 - 对象的
类(class)是一个表示对象类型的字符串
对象的区分
- 内置对象 ECMAScript规范定义的对象或类。例如数组、函数、日期等等
- 数组对象 根据JS解释器所嵌入的宿主环境(例如Web浏览器)决定
- 自定义对象 运行中的JS代码创建的对象
- 只有属性 直接在对象中定义的属性
- 继承属性 在对象原型中定义的属性
创建对象
创建对象有3种方法:
- 对象直接量
- 关键字
new Object.create()
Object.toLocaleString()
- 默认返回
toString()的结果 - 可以进行扩展以实现特定对象转换成字符串的定制化
数组
可以使用负数或者非整数作为索引,这种情况下数值会转换为字符串,所以数组不过是一个特殊定制化的对象
稀疏数组和密集数组
一般数据从0开始索引,如果值不连续则称为稀疏数组
稀疏数组可以很明显地看出内存利用率高而查找比稠密数组要慢的特性
对于稀疏数组查找元素的时间和常规对象相同
稀疏数组的对于不同浏览器有不同的实现
函数
构造函数调用
使用new 关键字的时候就调用了构造函数
以下两种方法是等价的:
var o =new Obj();
var o =new Obj;
构造函数里面会使用this关键字来实现对新创建的对象的引用
var a = new o.m(); //这时候上下文就不是o了
函数间接调用
- call()
- apply()
[待补充]
实参和形参
可选形参
- 如果调用时传入的实参比形参少则会将对应形参设置为
undefined(不是null!) 因此对未赋值的形参进行判断的时候最好使用===有一种很好的用法
这样可以将未赋值的形参a = a || defaultValue;a赋予一个默认值
可变长度的实参列表
函数中可以通过arguments来获取所有参数列表,这是一个实参对象(长得很像数组而已),因此可以通过length属性获取传入的参数个数
不会有人傻到定义一个变量叫做
arguments吧
类和类型
类的检测
有三种方法用于检测对象类:
instanceof/isprototypeof
缺点:
- 无法通过对象获得类名,只能检测对象是否属于特定类
- 多窗口和多框架的Web应用中兼容存在问题
Constructor
function typeAndValue(x) {
if (x == null) return "";
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;
}
}
缺点:
- 多窗口和多框架的Web应用中兼容存在问题
注意case后面的表达式都是函数。如果使用typeof的话获取到的结果会是字符串,例如下文
function type(o) {
var t, c, n; // type, class, name
// Special case for the null value:
if (o === null) return "null";
// Another special case: NaN is the only value not equal to itself:
if (o !== o) return "nan";
// Use typeof for any value other than "object".
// This identifies any primitive value and also functions.
if ((t = typeof o) !== "object") return t;
// Return the class of the object unless it is "Object".
// This will identify most native objects.
if ((c = classof(o)) !== "Object") return c;
// Return the object's constructor name, if it has one
if (o.constructor && typeof o.constructor === "function" && (n = o.constructor.getName())) return n;
// We can't determine a more specific type, so return "Object"
return "Object";
}
// Return the class of an object.
function classof(o) {
return Object.prototype.toString.call(o).slice(8, -1);
};
// Return the name of a function (may be "") or null for nonfunctions
Function.prototype.getName = function () {
if ("name" in this) return this.name;
return this.name = this.toString().match(/function\s*([^(]*)\(/)[1];
};
此外并非所有对象都有Constructor属性,匿名函数就是个典型:
// This constructor has no name
var Complex = function (x, y) {
this.r = x;
this.i = y;
}
// This constructor does have a name
var Range = function Range(f, t) {
this.from = f;
this.to = t;
}
JS的面向对象技术
一个全面并且典型的纯OOP例子:
function Set() { // This is the constructor
this.values = {}; // The properties of this object hold the set
this.n = 0; // How many values are in the set
this.add.apply(this, arguments); // All arguments are values to add
}
// Add each of the arguments to the set.
Set.prototype.add = function () {
for (var i = 0; i < arguments.length; i++) { // For each argument
var val = arguments[i]; // The value to add to the set
var str = Set._v2s(val); // Transform it to a string
if (!this.values.hasOwnProperty(str)) { // If not already in the set
this.values[str] = val; // Map string to value
this.n++; // Increase set size
}
}
return this; // Support chained method calls
};
// Remove each of the arguments from the set.
Set.prototype.remove = function () {
for (var i = 0; i < arguments.length; i++) { // For each argument
var str = Set._v2s(arguments[i]); // Map to a string
if (this.values.hasOwnProperty(str)) { // If it is in the set
delete this.values[str]; // Delete it
this.n--; // Decrease set size
}
}
return this; // For method chaining
};
// Return true if the set contains value; false otherwise.
Set.prototype.contains = function (value) {
return this.values.hasOwnProperty(Set._v2s(value));
};
// Return the size of the set.
Set.prototype.size = function () {
return this.n;
};
// Call function f on the specified context for each element of the set.
Set.prototype.foreach = function (f, context) {
for (var s in this.values) // For each string in the set
if (this.values.hasOwnProperty(s)) // Ignore inherited properties
f.call(context, this.values[s]); // Call f on the value
};
Set._v2s = function (val) { //这是一个内部函数,当然实例对象无法调用这个方法
switch (val) {
case undefined:
return 'u'; // Special primitive
case null:
return 'n'; // values get single-letter
case true:
return 't'; // codes.
case false:
return 'f';
default:
switch (typeof val) {
case 'number':
return '#' + val; // Numbers get # prefix.
case 'string':
return '"' + val; // Strings get " prefix.
default:
return '@' + objectId(val); // Objs and funcs get @
}
}
// For any object, return a string. This function will return a different
// string for different objects, and will always return the same string
// if called multiple times for the same object. To do this it creates a
// property on o. In ES5 the property would be nonenumerable and read-only.
function objectId(o) {
var prop = "|**objectid**|"; // Private property name for storing ids
if (!o.hasOwnProperty(prop)) // If the object has no id
o[prop] = Set._v2s.next++; // Assign it the next available
return o[prop]; // Return the id
}
};
Set._v2s.next = 100; // Start assigning object ids at this value.
另一种通过返回值设定类的方法
function Test() {
var map = 1;
function a(){
map = 2;
}
function b(){
console.log(map);
}
return{
a:a,
b:b
}
}
var t = new Test()
对于后者:
- 注意如果最后的return里面包含了map那么无论如何执行b()这个map的值都不会变, 因为返回的是一个Obj是额外空间
- 当然这里也可以不放返回值
- 返回值的方法是为了闭合部分接口
- 更大的区别是:很难重写第二种模式里面的方法
子类
原书中的子类内容比较累赘,可以归纳为以下几步:
-
继承prototype中定义的属性和方法;
-
继承构造函数中定义的属性和方法;
-
修改子类的prototype对象的constructor指针
function Animal(name) { this.name = name; } Animal.prototype.set = "female"; Animal.prototype.info = function () { console.log("animal"); } function People(name) { this.name = name; } People.prototype = new Animal("animal"); // 继承父类中定义的属性和方法; People.prototype.info = function() { //重写父类中定义的属性和方法; console.log("peopel") }; //Demo var cat = new Animal('cat'); console.log(cat instanceof Animal); //t console.log(cat instanceof Object); //t console.log( typeof Animal.prototype); //object console.log( typeof Animal.constructor); //function console.log(Animal.prototype.constructor == Animal); //true var mike = new People("mike"); console.log(mike.sex);//female mike.info();//People console.log(mike instanceof People); //t console.log(mike instanceof Animal); //t console.log(mike instanceof Object); //t console.log( typeof Animal.prototype); //object console.log( typeof Animal.constructor); //function console.log(People.prototype.constructor == People); //true
类的封装
简单封装方法:
-
使用
var关键字设置私有属性 -
阻止类的扩展:
使用
Object.seal()可以阻止给对象添加属性并将已有的属性设置为不可配置的,即不可删除但是这种情况下依然可以修改属性
Object.seal(mike); mike.sex = 'male'; //仍然可以修改 delete mike.sex; //Cannot delete property 'sex' -
阻止类的修改:
和
Object.seal()类似不过Object.freeze方法将实例方法设置为不可写的这种情况下修改对应方法将变得无效
Object.seal(mike); mike.sex = 'male'; //不会报错但是修改无效
模块化模式
首先我们来看看Module模式的基本特征:
- 模块化,可重用
- 封装了变量和function,和全局的namaspace不接触,松耦合
- 只暴露可用public的方法,其它私有方法全部隐藏
基本用法
var Calculator = function (eq) {
//这里可以声明私有成员
var eqCtl = document.getElementById(eq);
return {
// 暴露公开的成员
add: function (x, y) {
var val = x + y;
eqCtl.innerHTML = val;
}
};
};
var calculator = new Calculator('eq');
calculator.add(2, 2);
匿名闭包
(function () {
// ... 所有的变量和function都在这里声明,并且作用域也只能在这个匿名闭包里
// ...但是这里的代码依然可以访问外部全局的对象
}());
注意,匿名函数后面的括号,这是JavaScript语言所要求的,因为如果你不声明的话,JavaScript解释器默认是声明一个function函数,有括号,就是创建一个函数表达式,也就是自执行,用的时候不用和上面那样在new了,当然你也可以这样来声明:
(function () {/* 内部代码 */})();
引用全局变量
获取全局变量到匿名函数域
(function ($, YAHOO) {
// 这儿$相当于全局的jQuery
} (jQuery, YAHOO));//这两个是全局变量, 我们把它们放到这儿说明使用这两个参数调用上面那个匿名函数
从匿名函数域设定全局变量
var blogModule = (function () {
var my = [1,2,3]
return my;//其实只要把这些变量返回回去就行了, 之后blogModule就相当于my这个变量
} ());
当然return也可以返回一个object
var blogModule = (function () {
var my = [1,2,3]
return {
my: my,
you: null
}
} ());
高级用法
对变量自身进行扩展
var blogModule = (function (my) { // 2. 这里接收到了传进来的blogModule并把blogModule命名为my
var AddPhoto = function () { // 3. 这里给my添加了个函数, 因此blogModule也多了个函数
console.log(123);
};
return {AddPhoto: AddPhoto};
} (blogModule)); //1. 这里将blogModule传了进去
blogModule.AddPhoto()// 4. 扩展完毕后就可以调用了
松耦合扩展
上面的扩展必须要先定义这个blogModule, 能否在未定义的时候初始化而在已定义的时候直接扩展来达到松耦合的目的呢:
var blogModule = (function (my) {
// 添加一些功能
return my;
} (blogModule || {}));
这样可以英一顺序加载module模式
紧耦合扩展
虽然松耦合扩展很牛叉了,但是可能也会存在一些限制,比如你没办法重写你的一些属性或者函数,也不能在初始化的时候就是用Module的属性。紧耦合扩展限制了加载顺序,但是提供了我们重载的机会,看如下例子:
var blogModule = (function (my) {
var oldAddPhotoMethod = my.AddPhoto;
my.AddPhoto = function () {
// 重载方法,依然可通过oldAddPhotoMethod调用旧的方法
};
return my;
} (blogModule));
子模块
blogModule.CommentSubModule = (function () {
var my = {};
// ...
return my;
} ());