学习总览
JavaScript
- 数据类型
- 变量
- 作用域
CSS
- CSS3选择器
学习内容
(1) 数据类型
类型检测
- typeof方法
- Object.prototype.toString
目前typeof可检测出的类型仅有如下几种:
- number
- string
- function
- boolean
- undefined
- object
- symbol
对于数组/正则表达式/日期等类型,typeof明显不能满足我们的需求。那么这个时候就需要使用到Object.prototype.toString来帮我们判断变量的具体类型了。值得注意的是在不同版本的ECMAScript中该方法检测类型的内部机制是有差别的。
在ES3以及ES5阶段,该方法是借助内部属性[[Class]]的值将数据类型以字符串形式拼接而成返回的。到了ES6阶段,[[Class]]被弃用,改换为[[NativeBrand]]内部属性。
ES3官方文档:
When the toString method is called, the following steps are taken:
- Get the [[Class]] property of this object.
- Compute a string value by concatenating the three strings "[object ", Result(1), and "]".
- Return Result(2).
简单翻译一下就是,当toString方法被调用时,会经过以下几个步骤:
- 获取当前对象的内置属性[[Class]]
- 将[object、第一步的属性值以及]这三段内容拼接成一个字符串
- 将第二步的字符串结果返回
ES5官方文档:
When the toString method is called, the following steps are taken:
- If the this value is undefined, return "[object Undefined]".
- If the this value is null, return "[object Null]".
- Let O be the result of calling ToObject passing the this value as the argument.
- Let class be the value of the [[Class]] internal property of O.
- Return the String value that is the result of concatenating the three Strings "[object ", class, and "]"
简单翻译一下就是,当toString方法被调用的时候,会经过以下几个步骤:
- 如果this的值是undefined,那么返回"[object Undefined]"
- 如果this的值是null,那么返回"[object Null]"
- 调用this值的toObject抽象方法,并将返回值赋给变量O
- 获取变量O的内置属性[[Class]]的值,并赋给变量class
- 将由[object、class变量的值以及]这三段内容拼成的结果以字符串格式返回
ES6官方文档:
When the toString method is called, the following steps are taken:
- If the this value is undefined, return "[object Undefined]".
- If the this value is null, return "[object Null]".
- Let O be ToObject(this value).
- Let isArray be IsArray(O).
- ReturnIfAbrupt(isArray).
- If isArray is true, let builtinTag be "Array".
- Else, if O is an exotic String object, let builtinTag be "String".
- Else, if O has an [[ParameterMap]] internal slot, let builtinTag be "Arguments".
- Else, if O has a [[Call]] internal method, let builtinTag be "Function".
- Else, if O has an [[ErrorData]] internal slot, let builtinTag be "Error".
- Else, if O has a [[BooleanData]] internal slot, let builtinTag be "Boolean".
- Else, if O has a [[NumberData]] internal slot, let builtinTag be "Number".
- Else, if O has a [[DateValue]] internal slot, let builtinTag be "Date".
- Else, if O has a [[RegExpMatcher]] internal slot, let builtinTag be "RegExp".
- Else, let builtinTag be "Object".
- Let tag be Get (O, @@toStringTag).
- ReturnIfAbrupt(tag).
- If Type(tag) is not String, let tag be builtinTag.
- Return the String that is the result of concatenating "[object ", tag, and "]".
简单翻译一下就是,当toString方法被调用时,会经过以下几个步骤:
- 如果this的值是undefined,那么返回"[object Undefined]"
- 如果this的值是null,那么返回"[object Null]"
- 对this的值进行toObject1抽象操作,将其转为对象类型,并将该操作的返回值赋给变量O
- 执行isArray(O),并将返回值赋给变量isArray
- 如果有报错,就返回错误,否则取出isArray的值
- 如果isArray值为true,则声明变量builtTag并将其赋值为"Array"
- 如果变量O是一个怪异字符串对象2,则声明变量builtTag并将其赋值为"String"
- 如果变量O拥有[[ParameterMap]]内部插槽,则声明变量builtTag并将其赋值为"Arguments"
- 如果变量O拥有[[Call]]内部方法,则声明变量builtTag并将其赋值为"Function"
10.如果变量O拥有[[ErrorData]]内部插槽,则声明变量builtTag并将其赋值为"Error"- 如果变量O拥有[[BooleanData]]内部插槽,则声明变量builtTag并将其赋值为"Boolean"
- 如果变量O拥有[[NumberData]]内部插槽,则声明变量builtTag并将其赋值为"Number"
- 如果变量O拥有[[DateValue]]内部插槽,则声明变量builtTag并将其赋值为"Date"
- 如果变量O拥有[[RegExpMatcher]]内部插槽,则声明变量builtTag并将其赋值为"RegExp"
- 否则,则声明变量builtTag为"Object"
- 获取变量O的toStringTag属性值,并赋值给变量tag
- 如果有报错,就返回错误,否则取出tag的值
- 如果tag的类型不是字符串就将前面的builtTag的值赋给tag
- 将[object、tag的值以及]三部分的内容拼接成字符串,作为结果返回
虽然这些过程看上去很复杂,但是我们其实不需要特别关心这些细节,只需要知道如果我们需要知道一个变量的详细类型时使用Object.prototype.toString是比typeof更靠谱的方法即可。
类型转换
根据《你不知道的JavaScript》(中)第四章,总结了以下几个部分:
- 抽象值操作
- 转字符串(toString)
- 转布尔值(toBoolean)
- 转数字(toNumber)
转字符串时,对于基本类型来说就是简单的字符串化,比如:
const null_str = String(null); // 'null'
const undefined_str = String(undefined); // 'undefined'
const true_str = String(true); // 'true'
const false_str = String(false); // 'false'
const num_str = String(123); // '123'
const big_num_str = String(10000000000000000000000000000000000000000); // 1e+40
const small_num_str = String(0.0000000000000000000000000000000000000001); // 1e-40
对于引用类型来说,如果自己没有自定义toString方法,则会直接调用原型链上的toString方法,比如:
const obj1 = {
toString: () => {
return 'toString By Myself';
}
};
console.log(String(obj1)); // 'toString By Myself'
const obj2 = {
name: 'obj2',
foo: () => {
return 'this is a test obj';
},
};
console.log(String(obj2)); // [object Object]
转布尔值时,JavaScript 中的值可以分为以下两类:
(1) 可以被强制类型转换为 false 的值(假值)
(2) 其他(被强制类型转换为 true 的值)(真值)
常见的假值:
- undefined
- null
- +0/-0/NaN
- false
- ""
真值就是假值列表之外的值。基本除了上述提及的假值之外的值,被布尔化之后都会返回true,所以我们只需要记住上述几个典型的假值即可。
转数字时,总体的规则整理如下:
布尔值只会转换为0(true)/1(false)、undefined转为NaN、null转为0;ToNumber 对字符串的处理基本遵循数字常量的相关规则 / 语法。处理失败时返回 NaN(处理数字常量失败时会产生语法错误)。不同之处是 ToNumber 对以 0 开头的十六进制数字符串并不按十六进制处理而是按照十进制(此处给出了对0开头的十六进数字与字符串的结果对比)。
例如:
// 对于数字的处理
Number(012); // 10,8进制
// 对于字符串的处理
对于普通对象,如果要转为数字,那么首先会去调用它的valueOf以及toString方法,哪一个方法返回的值能转为数字,则使用该方法的返回值进行数字类型的抽象转换。例如下列代码中的数组[],其valueOf返回的是引用类型,而toString返回的是空字符串,所以空字符串经过toNumber抽象操作后返回0,也就是空数组转数字的结果。
- 显式强制类型转换
(1)字符串和数字之间的显式转换
字符串和数字之间的转换最熟悉的莫过于String(..)以及Number(..)(String的转换规则严格遵循前面提及的toString抽象操作,Number同理遵循toNumber抽象操作),此外还有toString()方法以及一元操作符+,不过此处需要注意一点,基本类型值使用toString方法时,实际上JavaScript引擎为其创建了一个封装对象,例如纯数字2使用toString时首先创建一个new Number(2)然后再去调用Number对象上的toString方法。
(2)显式解析数字字符串
解析字符串中的数字和将字符串强制类型转换为数字的返回结果都是数字。但解析(parseInt)和转换(Number)两者之间还是有明显的差别。
二者差别:解析允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停止。而转换不允许出现非数字字符,否则会失败并返回 NaN。
parseInt虽然有很多坑,但在详细了解之后,其实很多令人疑惑的地方都可以得到正确的答案。首先,我们需要规范parseInt的使用方式,传递参数值时限制为字符串格式,传递其他类型的值是无效的;其次,parseInt的第二个参数很重要,从ES5起,parseInt的第二个参数在不传递的时候就默认为十进制;最后,parseInt总是倾向于将传递进来的参数先转为字符串格式然后进行解析,所以会出现解析某些对象的时候直接调用了其自定义的toString方法的现象出现。
以上代码中列举了一个很著名的例子,parseInt(1/0, 19)返回18。
具体原因分析:
parseInt中得到的1/0的值为Infinity,在解析过程中,Infinity在解析到第二个字母时就已经不是有效的数字字符了,所以此处会直接截断,也就是说parseInt('Infinity', 19)等同于parseInt('I', 19),以19进制来计算时,我们可以借助10进制来类推。
最高进制转换
Base 36进制 | Base 10进制 |
---|---|
1 | 1 |
2 | 2 |
3 | 3 |
4 | 4 |
5 | 5 |
6 | 6 |
7 | 7 |
8 | 8 |
9 | 9 |
a | 10 |
b | 11 |
c | 12 |
d | 13 |
e | 14 |
f | 15 |
g | 16 |
h | 17 |
i | 18 |
j | 19 |
k | 20 |
l | 21 |
m | 22 |
n | 23 |
o | 24 |
p | 25 |
q | 26 |
r | 27 |
s | 28 |
t | 29 |
u | 30 |
v | 31 |
w | 32 |
x | 33 |
y | 34 |
z | 35 |
从上述这个对比表中可以看到,如果在高级别的进制中,i对应的正好是18,所以这个令人疑惑的问题瞬间解决了,当然可能还有人会没有反应过来,可以参考下面的代码。
虽然书中提及parseInt可识别的数字范围是0-9 a-i但是,我自己尝试过后,发现,parseInt('z', 36)都是可以正常返回35的,所以总结了上面的表格,这是一个令人惊讶的发现,后来我又尝试了一些奇奇怪怪的用法,例如:parseInt('hello', 36),guess what?它返回了29234652。当然,在实际生产中,还是尽量规范的使用parseInt,杜绝这些容易使他人疑惑的用法。
示例:
parseInt('a', 10); // NaN
parseInt('a', 11); // 10
parseInt('z', 36); // 35
parseInt('hello', 36); // 29234652
结论:只要在可用的进制范围内,那么一些稀奇古怪的字母字符串总是能被解析出来。
(3)显式转换为布尔值
虽然说显式转字符串和显式转数字会频繁用到Number和String,但是显式转为布尔值却不会时常用到Boolean,可以说大部分时候我们在转换布尔值时都是在隐式转换,比如以下几种情况下:
1、 if (..) 语句中的条件判断表达式。
2、for ( .. ; .. ; .. ) 语句中的条件判断表达式(第二个)。
3、while (..) 和 do..while(..) 循环中的条件判断表达式。
4、? : 中的条件判断表达式。
5、逻辑运算符 ||(逻辑或)和 &&(逻辑与)左边的操作数(作为条件判断表达式)。
- 隐式强制类型转换
隐式强制类型转换指的是那些隐蔽的强制类型转换,副作用也不是很明显。换句话说,你自己觉得不够明显的强制类型转换都可以算作隐式强制类型转换。隐式强制类型转换的作用是减少冗余,让代码更简洁,但同时这些悄悄发生的类型转换也会有一定程度的性能消耗,例如:非字符串类型的变量与字符串类型变量进行加号拼接时,一定会有一个非字符串类型转为字符串类型的过程,然后才能开始真正的拼接。
隐式转换的几种常见情况:
(1) 字符串和数字之间的隐式强制类型转换(参考加减乘除中字符串与数字的转换)
(2) 布尔值到数字的隐式强制类型转换(参考加减乘除中字符串与数字的转换)
(3) 隐式强制类型转换为布尔值(具体可以参考上一步显式转为布尔值中隐式转换情况举例部分)
(4) || 和 &&
在JavaScript中这两个逻辑操作符的返回值不一定是布尔值,还有可能是两个参数中的一个。
(5) 符号的强制类型转换
目前我们介绍的显式和隐式强制类型转换结果是一样的,它们之间的差异仅仅体现在代码可读性方面。
但 ES6 中引入了符号类型,它的强制类型转换有一个坑,在这里有必要提一下。ES6 允许从符号到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误。符号不能够被强制类型转换为数字(显式和隐式都会产生错误),但可以被强制类型转换为布尔值(显式和隐式结果都是 true)。
由于规则缺乏一致性,我们要对 ES6 中符号的强制类型转换多加小心。 - == 以及 ===的运用
==会进行类型的转换,而===不会。一般在生产环境要使用后者保证不会因为隐式类型转换导致一些不好掌控的bug。
(2) 变量
变量的数据类型
基本类型值以及引用类型值,基本类型值指的是简单的数据段,而引用类型值指的是可能由多个值构成的对象。在红宝书中提到数据类型的部分,解释的内容大概包含四部分:引用类型会有可以变更的动态属性,而基本类型则没有;引用类型在复制的时候传递的是引用地址,而基本类型在复制的时候传递的是值;函数参数的传递方式;类型判断(前面已经有大量篇幅在讲述,此处不赘述)。
重点需要学习和阐述的是第三部分函数参数的传递方式,在红宝书中传达给读者的值传递方式基本上就两种:按值传递、按引用传递。但是书中同时也提到了「按引用传递」这种说法是不太严谨的,原因在于如果真的是按照引用传递的,那么所有的操作实际上都是在操作对象的引用,但是当我们直接去增加/删除对象的属性时,被操作的就不再是对象的引用了,而是对象本身,这会让人十分疑惑。那还是看两段代码吧
(3) 作用域
执行环境定义了变量和函数有权访问的其他数据,决定了他们的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都会保存在这个对象中,即成为这个对象的属性或者方法。
(1)作用域是什么?
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。
(2)什么是LHS/RHS查询
const a = 2; 此时我们在对a变量进行赋值,那么可以称这个过程为LHS查询,我们只关心是否能为a赋值,而不关心这个值是什么;const b = a; 此时我们在给b赋值之前,需要先知道a是什么,会有一个追溯值的源头的过程,我们在乎的是a这个变量中存储的是什么,那么可以称这个过程为RHS查询。
以上就是简单的对于LHS/RHS查询进行的一个简单介绍。
(3)作用域是怎么工作的?
以web浏览器环境为示例,假设在Window宿主对象中,Window中有一个函数名为fnc,在函数test中又有一个名为insideFnc的函数,那么现在就有三层作用域,最外层是全局Window作用域,fnc函数中会有一个fnc自己的块级作用域,在insideFnc中又会有一层insideFnc的块级作用域,层层嵌套。且只允许从里向外查找,不允许从外向里查找,并且查找一旦到达最外层,无论此时有没有查找到对应的变量,查找工作都会立即停止。
(4)严格/非严格模式下的表现
无论是严格模式还是非严格模式,一旦RHS查询失败都会抛出引用错误ReferenceError,而LHS在非严格模式下,如果查询变量失败,那么全局作用域会悄悄创建一个全局变量并将其返还给引擎,继续后面的赋值操作;LHS在严格模式下就不会有这么幸运了,一样会抛出一个引用错误ReferenceError。
代码演示
// LHS查询
// 非严格模式
function test(b) {
a = b;
return a;
}
test(123); // 不报错,并且创建一个全局变量a
// 严格模式
'use strict';
function anotherTest(b) {
a = b;
return a;
}
anotherTest(); // ReferenceError,严格模式不会静默地创建全局变量
// RHS查询
function test1() {
const a = b;
return a;
}
test1(); // 报出ReferenceError,表明找不到b变量,溯源失败
// RHS查询
function test2(b) {
if (b) {
b();
} else {
return 'there is no arguments';
}
}
test2(123); // 报出TypeError,表明作用域中已经成功查找到变量,但是操作该变量时出现了类型错误,很明显这里是将数字当做函数执行了
(4)CSS新增内容
CSS3中新增内容,我主要想从两个方面学习,首先是新增属性部分,其次是新增选择器部分。这些新增的内容,必然会存在兼容性以及实用性上的优劣问题。
以下《图解CSS》(大漠老师著)给出的新属性列表总览:
本次的学习内容主要是选择器,具体内容如下:
(1) CSS3选择器
基本选择器:
- id选择器
- class选择器
- 标签选择器
- 通用选择器
- 群组选择器(个人更倾向于称之为“并集选择器”)
以上这些常用的选择器只做一个大概的罗列,具体的使用不再赘述。我会具体的选择器,主要包括以下几种:
- 伪类选择器
- 动态伪类
- 目标伪类
- 语言伪类
- UI元素状态伪类
- 结构伪类
- 否定伪类
动态伪类选择器
主要分为五部分:link/active/focus/visited/hover,这几个伪类选择器我们平时也是常见的 ,不再赘述。
目标为类选择器
E:target
,该选择器表示选择匹配E的所有元素并且匹配元素被相关URL指向。
代码:
// 样式
p:target {
background-color: gold;
}
/* 在目标元素中增加一个伪元素*/
p:target::before {
font: 70% sans-serif;
content: "►";
color: limegreen;
margin-right: .25em;
}
/*在目标元素中使用italic样式*/
p:target i {
color: red;
}
// HTML片段
Table of Contents
- Jump to the first paragraph!
- Jump to the second paragraph!
- This link goes nowhere,
because the target doesn't exist.
My Fun Article
You can target this paragraph using a
URL fragment. Click on the link above to try out!
This is another paragraph, also accessible
from the links above. Isn't that delightful?
效果图:
语言伪类选择器
E:lang(language)
,该选择器允许为不同的语言定义特殊的规则,在多语言版本的网站使用起来是很方便的。
代码:
// 当网站为英文时
This is the title of the WebSite
build a future where people live in harmony with nature
we hope they succeed.
// 当网站为中文时
作用域
这是网站标题
这是网站内容
另一段占位文字
效果图:
UI元素状态伪类选择器
常见的状态伪类选择器主要有三类:E:checked(选中状态)、E:enabled(启用状态)、E:disabled(不可用状态),这三个当中最常用的是checked以及disabled,而enabled较为少用,废话不多说,看示例。
代码:
效果图:
结构伪类选择器
这一类选择器主要的使用场景就是借助DOM树形结构去选中符合规律的某些节点,常见的有:
选择器 | 功能描述 |
---|---|
E:first-child | 作为父元素的第一个子元素出现的元素E,等于E:nth-child(1) |
E:last-child | 作为父元素的最后一个子元素出现的元素E,等于E:nth-last-child(1) |
E:root | 作为匹配元素E所在文档的根元素。在HTML中,根元素始终都是html |
F E:nth-child(n) | 选择父元素F的第n个子元素E,n的值是从1开始的,不是从0开始的 |
F E:nth-last-child(n) | 选择父元素F的倒数第n个子元素E,n的值同上一个选择器一致 |
E:nth-of-type(n) | 选择父元素内具有指定类型的第n个元素 |
E:nth-last-of-type(n) | 选择父元素内具有指定类型的倒数第n个元素 |
E:first-of-type | 选择父元素内具有指定类型的第1个元素 |
E:last-of-type | 选择父元素内具有指定类型的倒数第1个元素 |
E:only-child | 选择作为独生子元素的E元素 |
E:only-of-type | 选择父元素只包含一个同类型的子元素,该子元素匹配E元素 |
E:empty | 选择没有子元素的元素,且该元素不包含任何文本节点 |
代码:
// HTML片段
- one
- two
- three
- four
- five
- six
abc
para
def
para
ghi
// 样式
li:first-child {
color: red;
}
li:nth-child(3) {
color: green;
}
li:last-child::after {
content: '';
display: inline-block;
margin-left: 5px;
width: 10px;
height: 10px;
border-radius: 50%;
background: gold;
}
li:nth-child(2n+1) {
list-style-type: circle;
background: rgba(0, 0, 0, .2);
}
ul:only-of-type {
text-decoration: underline;
}
div div:first-of-type {
color: blue;
}
div div:last-of-type {
font-weight: bolder;
font-size: 24px;
}
p:nth-of-type(2) {
color: purple;
}
效果图:
否定伪类选择器
E:not(F),顾名思义否定即排除某个条件后剩余匹配到的元素。
代码:
// HTML片段
// CSS
input:not([type=checkbox]) {
background: black;
}
效果图:
引用文章
- ES6官方文档
- 怪异对象、普通对象
- 读懂ES6规格