你根本不懂Javascript(EP1~EP3.5 基础课)
做全栈许久, 因为负责的方面多因此各语言都没有掌握精髓。因此准备翻看一些真正权威并且全面的书进行查缺补漏,计划从前端开始玩,近日在读《Javascript 权威指南》,发觉自己居然有如此之多的部分全然不知,不禁一句感叹「你根本不懂 Javascript!」
原本这只是个人笔记,记录一些自己错过的基础知识。和某大牛交谈过程中,被推荐将笔记公开并创造与大众交流心得的可能性,故将此文发布了出来。
ECMAScript 有 5 种原始类型(primitive type)
- Undefined
- Null
- Boolean
- Number
- String
基本类型(null, undefined, bool, number, string)应该是值类型,没有属性和方法。
Javascript 有一系列内置对象来创建语言的基本功能,具体有如下几种
- 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 对象是一个数值包装器,该对象包含几个只读属性:
- 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
对象只有一个只读的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() 方法返回这个函数的实现定义的表示方式。通常是将用户定义的函数转换为 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 方法会返回一个内部表示:1970 年 1 月 1 日以来的毫秒数
通常情况下对象是通过 toString()和 valueOf()方法,就可以做到对象到字符串和对象到数字的转换。
- 如果具有 toString()方法,则调用这个方法,如果它返回一个原始值,js 将其转换为字符串,并返回这个字符结果。
- **如果没有 toString()或者这个方法并不返回一个原始值,那么 js 会去调用 valueOf()。**如果有调用它,如果返回值是原始值。则将其转换成字符串。3. 如果没有 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. 直接量
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 后的数值重新赋值给变量,数组元素或者对象属性。
==
运算符用于检测两个操作数是否相等,这里的比较很宽松因为允许了类型转换,检测室会通过如下逻辑:1. 如果一个值是null
另一个是undefined
,则相等
- 如果一个是数字一个是字符串,字符串转为数字再比较
- 如果是
true
则转换成 1,false
转换成 0 - 如果一个值是对象另一个是数字或字符串,对象则转换成原始值(参考上文逻辑,注意日期类的例外情况)
===
的检测就比较严格,会通过如下逻辑:1. 如果两个值类型不同,则不相等
- 如果两个值都是
null/undefined
,则不相等 - 如果两个值都是布尔值
true
或者都是布尔值false
,则相等 - 如果一个是
NaN
或者都是NaN
,则不相等(NaN
与任何值都不相等) - 如果都是数字并且值相等,则相等
- 如果都是字符串并且对应 16 位值相同,则相等
- 如果两个引用值指向同一个对象,则相等
包含各种>
,<
,>=
,<=
等比较运算符的运算逻辑:1. 如果操作数为对象,转换成原始值
- 转换后如果都是字符串那么按照字母表顺序比较
- 转换后如果至少一个不是字符串,那么两个都转为数字进行比较
- 如果转换后一个值是
NaN
那么一定返回false
typeof 也是一个运算符!
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 种方法进行属性检测:1. in
运算符
``` var o = {x:1}; "x" in o;//true "y" in o;//false "toString" in o;//true ```
2. hasOwnPreperty()
3. PropertyIsEnumerable()
对象有对应属性并且可枚举性为`true`才返回`true` >内置属性是不能枚举的
4. 通过!==
判断属性是否为undefined
``` o.x!==undefined//true o.y!==undefined//false ```
- 对象的
原型(prototype)
指向另一个对象,本对象的属性都继承于原型对象 - 对象的
类(class)
是一个表示对象类型的字符串
- 内置对象 ECMAScript 规范定义的对象或类。例如数组、函数、日期等等
- 数组对象 根据 JS 解释器所嵌入的宿主环境(例如 Web 浏览器)决定
- 自定义对象 运行中的 JS 代码创建的对象
- 只有属性 直接在对象中定义的属性
- 继承属性 在对象原型中定义的属性
创建对象有 3 种方法:1. 对象直接量
2. 关键字new
3. Object.create()
- 默认返回
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
吧
有三种方法用于检测对象类:
缺点:
- 无法通过对象获得类名,只能检测对象是否属于特定类
- 多窗口和多框架的 Web 应用中兼容存在问题
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; }
一个全面并且典型的纯 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 是额外空间
- 当然这里也可以不放返回值
- 返回值的方法是为了闭合部分接口
- 更大的区别是:很难重写第二种模式里面的方法
原书中的子类内容比较累赘,可以归纳为以下几步:1. 继承 prototype 中定义的属性和方法;1. 继承构造函数中定义的属性和方法;1. 修改子类的 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 ```
简单封装方法:1. 使用var
关键字设置私有属性
2. 阻止类的扩展:
使用`Object.seal()`可以`阻止给对象添加属性并将已有的属性设置为不可配置的,即不可删除` 但是这种情况下依然可以修改属性 ``` Object.seal(mike); mike.sex = 'male'; //仍然可以修改 delete mike.sex; //Cannot delete property 'sex' ```
3. 阻止类的修改:
和`Object.seal()`类似不过`Object.freeze`方法将实例方法设置为不可写的 这种情况下修改对应方法将变得无效 ``` Object.seal(mike); mike.sex = 'male'; //不会报错但是修改无效 ```
首先我们来看看 Module 模式的基本特征:1. 模块化,可重用 2. 封装了变量和 function,和全局的 namaspace 不接触,松耦合 3. 只暴露可用 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; } ());
关于本文
文章标题 | 你根本不懂Javascript(EP1~EP3.5 基础课) |
发布日期 | 2017-02-18 |
文章分类 | Tech |
相关标签 | #JS |
留言板
PLACE_HOLDER
PLACE_HOLDER
PLACE_HOLDER
PLACE_HOLDER