你不知道的JavaScript(中卷)一

一、类型

1.对语言引擎和开发人员来说,类型是值的内部特征,它定义了值的行为,以使其区别于其他值

A.内置类型

1.七种:null、undefined、boolean、number、string、object、symbol

2.null类型:typeof null === “object”

3.typeof function a(){} === “function”,function是object的一个“子类型”,函数是可调用对象,它有一个内部属性[[call]],该属性可以被调用

C.值和类型

1.JS中的变量是没有类型的,只有值才有。变量可以随时持有任何类型的值。JS不要求变量总是持有与其初始值同类型的值。对变量执行typeof时,结果并不是该变量的类型,而是该变量持有的值的类型

2.已在作用域中声明但还没有赋值的变量,是undefined的。相反,还没有在作用域中声明过的变量,是undeclared的。typeof都会返回undefined。直接打印”undeclared”会产生ReferenceError错误

3.通过typeof的安全防范机制(阻止报错)来检查undeclared变量,if(typeof atob===“undefined”)、if(!window.atob)等

二、值

A.数组

1.在JS中,数组可以容纳任何类型的值,可以是字符串、数字、对象(object),甚至是其他数组

2.使用delete运算符可以将单元从数组中删除,但是请注意,单元删除后,数组的length属性并不会发生变化

3.类数组,一些DOM元素,arguments对象(ES6已经废止),使用Array.prototype.slice.call(arguments)可以转化为真正的数组,ES6中的Array.from(arguments)也可以实现

B.字符串

1.字符串和数组很相似

2.JS中字符串是不可变的,而数组是可变的。应该使用a.charAt(1)取下标位置的字符

3.字符串不可变是指字符串的成员函数不会改变其原始值,而是创建并返回一个新的字符串。而数组的成员函数都是在其原始值上进行操作

4.简单粗暴的反转字符串方法,a.split(“”).reverse().join(“”),转为数组,反转后再转回字符串

C.数字

1.JS中只有一种数值类型:number,包括“整数”和带小数的十进制数。JS没有真正意义上的整数,“整数”就是没有小数的十进制数

2.数字的语法

• toExponential()输出指数格式,数字值可以使用Number.prototype中的方法

• toFixed():指定小数部分的显示位数

• toPrecision():指定有效数位的显示位数

• 对于.运算符需要注意,因为它是一个有效的数字字符,会被优先识别为数字常量的一部分,然后才是对象属性访问运算符,可以使用..或 .,如42..toFixed()或42 .toFixed()

3.较小的数值:二进制浮点数不精确,如0.1+0.2===0.3(false),尽量使用整数

4.整数的安全范围:Number.MAX_VALUE是2^52-1,在ES6中被定义为Number.MAX_SAFE_INTEGER,最小整数为Number.MIN_SAFE_INTEGER

5.整数检测:ES6中使用Number.isInteger()方法,可以polyfill,typeof num == “number” && num %1==0;

D.特殊数值

1.不是值的值

• undefined:没有值(missing value),从未赋值是一个标识符,可以被当作变量来使用和赋值(永远不要重新定义undefined)

• null:空值(empty value),曾赋过值,但是目前没有值,不是标识符,不能当作变量来使用和赋值

• void:表达式void __没有返回值,并不改变表达式的结果,只是让表达式不返回值,在某些情况下很有用

2.特殊的数字

• NaN:意指“不是一个数字”,理解为“无效数值”“失败数值”或者“坏数值”更准确,NaN是一个警戒值(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”,它和自身不相等,是唯一一个非自反(reflexive,即x===x不成立)的值,ES6中使用Number.isNaN()

• 无穷数:JS使用有限数字表示法(finite numeric representation),所以和纯粹的数学运算不同,JS的运算结果有可能溢出,此时结果为Infinity或者-Infinity,计算结果一旦溢出为无穷数(infinity)就无法再得到有穷数,infinity/infinity==NaN,1/infinity==0

• 零值:JS中有0和-0

3.特殊等式:ES6中加入了Object.is()方法判断两个值是否绝对相等,可以用来处理NaN或者-0,便能使用==和===时就尽量不要使用Object.is()

E.值和引用

1.引用就像一种特殊的指针,是来指向变量的指针(别名),如果参数不声明为引用的话,参数值总是通过值复制的方式传递,即使对复杂的对象值也是如此。

2.JS中没有指针,引用的工作机制也不尽相同。

3.JS中引用指向的是值。如果一个值有10个引用,这些引用指向的都是同一个值,它们相互之间没有引用/指向关系。

4.JS对值和引用的赋值/传递在语法上没有区别,完全根据值的类型来决定。

5.简单值(即标题基本类型值,scalar primitive)总是通过值复制的方式来赋值/传递,包括null、undefined、字符串、数字、布尔和ES6中的symbol

6.复合值(compound value)—对象(包括数组和封装对象)和函数,总是通过引用复制的方式来赋值/传递

7.由于引用指向的是值本身而非变量,所以一个引用无法更改另一个引用的指向

8.请记住:我们无法自行决定使用值复制还是引用复制,一切由值的类型来决定

9.如果通过值复制的方式来传递复合值(如数组),就需要为其创建一个香醇,这样传递的就不再是原始值;如果要将标量基本类型值传递到函数内并进行更改,就需要将该值封装到一个复合值(对象、数组等)中,然后通过引用复制的方式传递;

三、原生函数

1.通过构造函数创建出来的是封装了基本类型值的封装对象

A.内部属性[[Class]]

1.所有typeof返回值为”object”的对象都包含一个内部属性[[Class]](我们可以把它看作一个内部的分类,而非传统的面向对象意义上的类)。这个属性无法直接访问,一般通过Object.prototype.toString(..)来查看。如:Object.prototype.toString.call([1,2,3]);

B.封装对象包装

1.封装对象(object wrapper)由于基本类型值没有.length和toString()这样的属性方法,需要通过封装对象才能访问,此时JS会自动为基本类型值包装(box或wrap)一个封装对象

2.一般情况下,我们不需要直接使用封装对象。最是让JS引擎自己决定什么时候应该使用封装对象。应该优先考虑使用42和”abc”这样的基本类型值,而非new String(“abc”)和new Number(42)

3.如果想要得到封装对象中的基本类型值,可以使用valueOf()函数,在需要用到封装对象中的基本类型值的地方会发生隐式拆封

C.原生函数作为构造函数

1.尽量避免合适构造函数,除非十分必要,因为它们经常会产生意想不到的结果

2.Array(..):使用new和不使用是一样的,如果只有一个参数会指定为数组长度,不同的浏览器开发控制台显示的结果也不尽相同。永远不要创建和使用空单元数组。

3.Object()、Function()和RegExp():除非万不得己,否则尽量不要使用这三种方式来创建对象

4.Date()和Error():比较有用

• Date()创建时必须带new,getTime()方法获得当前时间,ES5后可以使用Date.now()方法

• Error()带不带new都可以,可以获得当前运行栈的上下文,通常与throw一起使用,一般包含一个message属性

5.Symbol():ES6中引入,具有唯一性的特殊值(并非绝对),用它来命名对象属性不容易导致重名,不能带new,主要用于私有或者特殊属性。是一种简单标量基本类型。

6.原生原型:如String.prototype.indexOf()等等。将原型作为默认值,Function.prototype是一个空函数,RegExp.prototype是一个“空”的正则表达式(任何匹配),Array.prototype是一个空数组。

四、强制类型转换

A.值类型转换

1.将值从一种类型转换为另一种类型通常称为类型转换(type casting),这是显式的情况;隐式的情况称为强制类型转换(coercion)

2.JS中的强制类型转换总是返回标量基本类型值,如字符串、数字和布尔值,不会返回对象和函数;“封装”,就是为标量基本类型值封装一个相应类型的对象,但这并非严格意义上的强制类型转换

3.类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在动态类型语言的运行时(runtime)

4.在JS中通常将它们统称为强制类型转换。显式强制类型转换可以从代码中看出,而隐式强制类型转换则不那么明显

5.例子:

var a = 42;

var b = 42+””;//隐式

var c = String(a);//显式

B.抽象值操作

1.ToString:负责处理非字符串到字符串的强制类型转换

• 基本类型值的字符串化规则为:null转换为”null”,undefined转换为”undefined”,true转换为”true”。数字遵循能用规则,极小和极大的娄子使用指数形式;对普通对象来说,除非自定义了toString方法,否则返回内部属性[[Class]]的值;数组经过了重新定义,将所有单元字符串化以后再用”,”连接起来;

• JSON字符串化:JSON.stringify()对于不安全的JSON值,undefined、function、symbol会自动忽略,在数组中则返回null;对于包含循环引用的对象则会出错;对于含有非法JSON值的对象做字符串化,需要定义toJSON方法来返回一个安全的JSON值;toJSON()应该“返回一个能够被字符串化的安全的JSON值”,而不是“返回一个JSON字符串”;

• JSON.stringify()有一个可选参数replacer,可以是数组或函数,用来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除,和toJSON很像;如果是数组必须是一个字符串数组,如果是函数,会传递键和值两个参数;

• JSON.stringify()还有一个可选参数space,用来指定输出的缩进格式

• JSON.stringify()并不是强制类型转换,但它涉及ToString强制类型转换:字符串、数字、布尔值和null的JSON.stringify()规则与ToString基本相同;如果传递给JSON.stringify()的对象中定义了toJSON()方法,那么该方法会在字符串化前调用

2.ToNumber:将非数字值转化为数字

• true转换为1,false为0,undefined为NaN,null为0;对字符串基本遵循数字常量的相关规则,失败时返回NaN,对以0开头的十六进制数并不按十六进制而是十进制处理;

• 对象(包括数组)会首先被转换为相应的基本类型值(调用ToPrimitive检查是否有valueOf(),如果没有就使用toString(),如果都不返回产生TypeError错误),再遵循上述元则强制转换为数字

4.ToBoolean

• JS中1和0与true和false并不是一回事

• 假值:undefined、null、false、+0-0和NaN、””

• 真值:可以理解为除假值以外的都是真值,对象默认全部为真值

• 假值对象:一些浏览器不使用或者淘汰的对象,例如document.all,常用来判断是否是老的IE

C.显式强制类型转换

1.字符串和数字之间的显式转换:通过String()和Number()来实现,不使用new,并不是创建封装对象;

• 一元运算符+-被普遍认为是显式强制类型转换,也可以将日期转换成Unix时间戳

• ~运算符:首先将值强制类型转换为32位数字,然后执行字位操作“非”(对每一个字位进行反转),~x大致等同于-(x+1),~42=-(42+1)=-43,可以配合indexOf转换为真假值,如~a.indexOf()当返回-1时为假值,其他情况下都会是真值

2.显式解析数字字符串

• 转换字符串:Number(…)不允许出现非数字字符,否则返回NaN

• 解析字符串:parseInt(...)可以出现非数字,从左到右,如果遇到非数字就停止,仅针对字符串值。

• ES5之前需要指定parseInt()的第二个参数,用于指定进制类型否则首字为x则转换为16进制,首字为0则转换为8进制

• parseInt(…)会先将参数强制类型转换为字符串再进行解析

3.显式转换为布尔值:使用Boolean()不用new,但最常用的是一元运算符!!

D.隐式强制类型转换

1.字符串和数字之间的隐式强制类型转换

• 根据ES5规范,如果某个操作数是字符串或者能够通过以下步骤转换为字符串的话,+将进行拼接操作。如果其中一个操作数是对象(包括数组),则首先对其调用ToPrimitive抽象操作,该抽象操作再调用[[DefaultValue]],以数字作为上下文

• 在定制valueOf()和toString()时需要非常小心,a+””这种会先调用valueOf()再调用toString(),而String()则直接调用toString()结果可能会不同

2.隐式强制类型转换为布尔

• if(..)语句中的条件表达式

• for(..;..;..)语句中的条件判断表达式(第二个)

• while(..)和do..while(..)循环中的条件判断表达式

• ?:中的条件判断表达式

• 逻辑关系运算符||(逻辑或)和&&(逻辑与)左边的操作数(作为条件判断表达式)

3.||和&&

• ES5规范:&&和||运算符的返回值并不一定是布尔类型,而是两个操作数其中一个的值。

• ||和&&首先会对第一个操作数执行条件判断,如果其不是布尔值就先进行ToBoolean强制类型转换,然后再执行条件判断

4.符号的强制类型转换

• ES6中引入的符号类型Symbol,允许从符号到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误。

• 符号不能够被强制类型转换为数字(显式和隐式都会产生错误),但可以被强制类型转换为布尔值(显式和隐式都是true)

E.宽松相等和严格相等

1.==允许在相等比较中进行强制类型转换,而===不允许

2.抽象相等

• ES5规范“抽象相等比较算法”定义了==运算符的行为。如果两个值的类型相同,就仅比较它们是否相等。对象(包括函数和数组)两个对象指向同一个值时即视为相等,不发生强制类型转换。==和===在比较对象上时是相同的。==在比较两个不同类型的值时会发生隐式强制类型转换,会将其中一或两者转换为相同的类型后再进行比较。注意:NaN!=NaN、+0==-0

• 字符串和数字之间的相等比较:字符串被强制转换为数字

• 其他类型和布尔类型之间的相等比较:boolean类型会执行ToNumber,所以”42”==true(”42”==1),尽量避免==ture和===ture,使用if(a)、if(!!a)、if(Boolean(a))

• null和undefined之间的相等比较:在==中,null和undefined是一样的,它们之间的强制类型转换是安全可靠的,if(a==null),null和undefined是成立的,但不包括0,false和””

• 对象和非对象之间的相等比较:会调用对象的ToPromitive抽象操作进行强制类型转换

3.比较少见的情况

• 返回其他数字:Number.prototype.valueOf = function(){return 3;};new Number(2)==3

• 假值的相等比较:”0”==false、false==0、false==“”、false==[]、”” == 0、””==[]、==[]

• 极端情况:[] == ![]([]==false),0==“\n”(ToNumber会强制转换为0)

4.安全运用隐式强制类型转换

• 如果两边的值中有true或false,千万不要使用==

• 如果两边的值中有[]、””或者0,尽量不要使用==

• 解决:上述情况使用===或者显式转换

• typeof操作总是返回七个类型字符串,比较安全

F.抽象关系比较

1.a<b比较双方首先调用ToPrimitive,如果结果出现非字符串,就根据ToNumber规则强制转换为数字,如果都是字符串,则按照字母顺序来比较

2.JS中的>=和<=实际上是“不大于”和“不小于的意思”,即a>=b === !(b>a),关系比较没有严格模式,因此要避免发生隐式强制类型转换

五、语法

A.语句和表达式

1.语句相当于句子,表达式相当于短语,运算符相当于标点符号和连接词

2.语句的结果值:获得结果值最直接的方法是在浏览器开发控制台中输入语句,默认情况下控制台会显示所执行的最后一条语句的结果值。var的结果值就是undefined,所以var a =42的结果值是undefined。

• 代码块{}的结果值是其最后一个语句/表达式的结果,如同一个隐式的返回,即返回最后一个语句的结果值,语法不允许我们获得语句的结果值并将其赋值给另一个变量,可以使用eval()来获取 ,ES7规范可以使用do{}表达式执行一个代码块

3.表达式的副作用:函数调用,++、—运算符等

• 可以使用,语句系统逗号运算符(statement-series comma operator)将多个独立的表达式串联成一个语句b = (a++,a)

• 链式赋值,如var a=b=42,如果b没有在作用域中象var这样声明过,则不会对变量b进行声明

4.上下文规则

• 标签跳转:foo:for(){for(){if(){continue foo;//直接跳最外层}}},可以替代其他语言的goto语句,break可以用于非循环代码块

• JSON是JS语言的一个真子集,但是JSON本身并不是合法的JS语法。JSON-P(将JSON数据封装为函数调用,比如foo({“a”:42}))通过将JSON数据传递给函数来实现对其的访问,能将JSON转换为合法的JS语法

• 代码块:[]+{}=“[object Object]”;{}+[]=0;因为{}被当作一个独立的代码块(不执行任何操作),然后再+[](显式转换为0)

• 对象解构:在ES6中可以对象解构,如var {a,b} = getData();

• JS中没有else if,实际上是if(){}else{if(){}else{}}这样

B.运算符优先级

1.短路:对&&和||来说,如果从左边的操作数能够得出结果,就可以忽略右边的操作数。这种现象称为短路(即执行最短路径)

2.&&>||>?:

3.关联:一般来说运算符的关联(associativity)是从左到右(JS的默认执行顺序),?:是右关联顺序

4.在编写程序时结合起来,即要依赖运算符优先级/关联规则,也要适当使用()自行控制方式

C.自动分号

1.自动分号插入(Automatic Semicolon Insertion,ASI):如果解析器发现代码可能因为缺失分号而导致错误,那么它就会自动补上分号。并且,只有在代码行末尾与换行符之间除了空格和注释之外没有别的内容时,它才会这样做。

2.建议在所有需要的地方加上分号,将对ASI的依赖降到最低

D.错误

1.运行时错误:TypeError、ReferenceError、SyntaxError等

2.编译阶段发现的代码错误叫作“早期错误”(early error)。另外,语法正确但不符合语法规则的情况也存在。

3.TDZ:ES6定义的(Temporal Dead Zone,暂时性死区),指的是由于代码中的变量还没有初始化而不能被引用的情况

E.函数参数

1.在ES6中,如果参数被省略或者值为undefined,则取该参数的默认值,函数不带参数时、传递undefined时,arguments数组的内容会不同

2.在严格模式中没有建立关联这一说,因此,在开发中不要依赖这种关联机制,实际上,它是JS语言引擎底层实现的一个抽象泄漏(leaky abstraction),并不是语言本身的特性。

3.arguments数组已经被废止,但并非一无是片,注意不要同时访问命名参数和其对应的arguments数组单元

F.try..finally

1.函数try中出现return也是先返回finally的内容,先执行try中的return并将函数的返回值设置为return的值,接着执行finally然后函数执行完毕

2.finally中的return会覆盖try和catch中return 的返回值

附录A.混合环境JavaScript

A.Annex B(ECMAScript)

1.Annex B,介绍了由于浏览器兼容性问题导致的与官方规范的差异。这些差异只存在于浏览器中,如果代码只在浏览器中运行,就不会发现任何差异。否则(如果代码也在Node.js、Rhino等环境中运行),或者你也不确定的时候,就需要小心对待

• 在非严格模式中允许八进制数值常量存在,如0123

• window.escape()和window.unescape()让你能够转义和回转带有%分隔符的十六进制字符串

• String.prototype.substr和String.prototype.substring十分相似,除了前者的第二个参数是结束位置索引,后者的第二个参数是长度

2.Web ECMAScript

• <!—和—>是合法的单行注释分隔符

• String.prototype中返回HTML格式字符串的附加方法:anchor()、big()、blink()、bold()、fixed()、fontcolor()、fontsize()、italics()、link()、small()、strike()和sub()

• RegExp扩展:RegExp.$1 .. RegExp.$9(匹配级)和RegExp.lastMatch/RegExp[“$&”](最近匹配)

• Function.prototype附加方法:Function.prototype.arguments(别名为arguments对象)和Function.caller(别名为arguments.caller)(均已废止,尽量不要使用)

B.宿主对象

• 注意的宿主对象的行为差异:

• 无法访问正常的object内建方法,如toString()

• 无法写覆盖

• 包含一些预定义的只读属性

• 包含无法将this重载为其他对象的方法

• 其他....

C.全局DOM变量

1.声明一个全局变量的结果不仅仅是创建一个全局变量,而且还会在global对象(在浏览器中为window)付了款一个同名属性

2.由于浏览器演进的历史遗留问题,在创建带有id属性的DOM元素时也会创建同名的全局变量

D.原生原型

1.不要扩展原生方法,除非你确信代码在运行环境中不会有冲突

2.在扩展原生方法时需要加入判断条件(因为你可能无意中覆盖了原来的方法)

3.shim/polyfill:如果规范中已经定义了prototype,并且功能和你的实现类似,那就没有什么问题,这种情况一般称为polyfill(或者shim),对于将来可能成为标准的功能,按照大部分人赞同的方式来预先实现能和将来的标准兼容的polyfill,称为prollyfill(probably fill)

E.<script>

1.多个<script>是独立的,但他们共享global(在浏览器是window),这些文件中的代码在共享的命名空间中运行,并相互交互

2.如果script中的代码发生错误,它会像独立的JS程序那样停止,但是后续的script中的代码(仍然共享global)依然会接着运行,不会受影响

3.可以使用代码来动态创建script,将其加入到页面的DOM中,效果是一样的,内联代码中不可以出现</script>字符串,应该使用”</sc”+”ript>”

4.需要注意外联script标签的charset,内联代码中的HTML或XHTML注释已废止

F.保留字

1.不要用作变量名、对象常量中的属性名称或者键值(目前已没有这个限制)

2.四类:“关键字”、“预留关键字”、null常量和true/false布尔常量

G.实现中的限制

• 字符串常量中允许的最大字符数(并非只是针对字符串值)

• 可以作为参数传递到函数中的数据大小(也称为栈大小,以字节为单位)

• 函数声明中的参数个数

• 未经优化的调用栈(例如递归)的最大层数,即函数调用链的最大长度

• JS程序以阻塞方式在浏览器中运行的最长时间(秒)

• 变量名的最大长度

原文发布于微信公众号 - 硬核项目经理(fullstackpm)

原文发表时间:2017-04-16

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券