《重学前端》的学习笔记
极客时间的专栏《重学前端》的学习笔记,持续更新中。
开篇词 | 从今天起,重新理解前端
前端开发之痛:散点自学 + 基础不牢,对于前端的知识体系和底层原理没有真正系统地理解
关于前端工程师成长需要两个视角:
(1)立足标准,系统性总结和整理前端知识,建立自己的认知和方法论;
(2)放眼团队,从业务和工程角度思考前端团队的价值和发展需要。
除此之外,前端工程师还需要了解程序员通用的编程能力和架构能力。
01 | 明确你的前端学习路线与方法
两个前端学习方法:
(1)建立知识架构
将零散的知识组织起来,也能帮助我们发现知识上的误区
(2)追本溯源
关注某个知识点背后的体系
03 | HTML语义:div和span不是够用了吗?
语义类标签的特点是在视觉表现上都差不多,主要区别在于表示了不同的语义。
语义标签的好处:(1)增强网页结构可读性;(2)更适合SEO和读屏软件
语义标签用不好会造成大量的冗余标签,所以“用对”比“不用”好,“不用”比“用错”好
比较重要的语义标签适用场景:
(1)作为自然语言延伸的语义类标签,例如<ruby>
、<rt>
、<rp>
(2)作为消除歧义的标签,例如表示重音的<em>
(3)作为标题摘要的标签,例如<h1>
、<hgroup>
、<section>
(4)作为整体结构的语义类标签。例如<header>
、<footer>
、<aside>
、<address>
1 | <body> |
04 | HTML语义:如何运用语义类标签来呈现Wiki网页?
用Wiki的一个页面举例,学习了语义化标签的使用。
这一节介绍到的语义化标签有:
aside
,导航性质的工具内容article
,有明确独立性的主体部分hgroup
/h1
/h2
,一个标题组中的各级标题abbr
,缩写的词语都应该使用abbr
标签hr
,横向分割线,但是表示故事走向或话题的转变,如果是单纯的视觉效果不应该使用这个标签strong
,强调的blockquote
,段逻辑的引述内容q
,行内的引述内容cite
,引述的作品名time
,日期,让机器阅读更方便figure
/figcaption
,与著文章先骨干的、有一定自包含性的内容,都可以用figure
包裹,可以是图片、表格、代码等,figcaption
是这些内容的标题dfn
,包裹被定义的名字,例如:<dfn>程序员</dfn>就是写程序的人
nav
,目录导航ol
/ul
,二者的区别是内容是否有顺序关系,不要因为视觉表现夏鸥改变语义pre
,表示其中的内容是预先经过排版的,不需要浏览器干预排版(保留了原来的缩进格式)samp
:计算机程序的实例输出code
:代码,和samp
一样,一般都是包裹在pre
之中
还有一些其他的标签:
date
,类似于time
,给及其阅读的内容sub
/sup
,下标/上标,多用于数学、物理、化学领域menu
,ul
的变体,用于功能菜单使用main
,整个页面出现一个,表示页面的主要内容,可以理解为特殊的div
语义化标签非常多,并且不像严谨的编程语言一样,有一条非此即彼的线。一些语义的引入会带来争议,所以应该尽量只用自己熟悉的语义标签,并且只在有把握的场景引入语义标签,保证语义不被滥用,不带来更多问题。
05 | JavaScript类型的细节
类型
JavaScript中定义了7种类型:
- Undefined
- Null
- String
- Number
- Boolean
- Symbol
- Object
Undefined/Null
JavaScript中的undefined
是一个变量,而非一个关键字。任何变量在赋值之前都是Undefined类型,值为undefined
,是一个全局变量,所以有:
1 | undefined === undefined; // true |
void
运算的作用是将一个表达式变为undefined
:
1 | let a = void 0; |
一般不会将变量赋值为undefined
,保证所有值为undefined
的变量都是从未赋值的自然状态
Null类型也只有一个值null
,所以有:
1 | null === null; // true |
Null语义是定义了,但是为空,这与Undefined是不同的
String
String的最大长度是2^53-1
,这个长度指的不是字符数,而是字符串的编码长度
字符串操作charAt
、charCodeAt
、length
针对的都是UTF16编码
UTF是Unicode的编码方式,一个Unicode码点表示一个字符,通常用U+????
来表示,其中????
是十六进制的码点值,0-65536(U+0000
-U+FFFF
的码点被称为基本字符区域
在JavaScript中的表示:
1 | '\u0031'; // 1 |
JavaScript字符串把每个UTF16单元当做一个字符串来处理,所以处理超出自己字符区域的字符时需要格外小心。
感觉阮一峰关于字符编码的知识讲的更加详细。
Number
Number类型符合IEEE754-2008规定的双精度浮点数规则,但是为了表达几个额外的语言场景,规定了几个例外情况
NaN
Infinity
,无穷大-Infinity
,负无穷大
Infinity
和-Infinity
是为了不让除以0
出错而引入的
JavaScript中有0
和-0
的区别,加法类运算没有区别,除法场合需要留意区分,区分的方式就是检测用作除数的结果,是Infinity
还是-Infinity
1 | 1/-0 ===-Infinity; // true |
由于浮点数运算的精度问题,导致非整数得Number不能直接比较:
1 | 0.1 + 0.2 === 0.3 // false |
正确的比较方法是使用JavaScript提供的最小精度之Number.EPSILION
1 | Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILION // ture |
==检查等式左右两边的差是否小于最小精度==,才是正确的比较浮点数的方法。
Symbol
Symbol是一切非字符串的类型key
的集合,使用全局的Symbol
函数创建Symbol
1 | var mySymbol = Symbol("my symbol"); |
一些特殊的属性可以在全局的Symbol函数的属性中找到,比如可迭代对象的迭代器就定义在了Symbol.iterator
属性上
1 | let o = {}; |
更多的内容需要专门学习。
Object
提出了一个问题,为什么给对象添加的方法能够用在基本类型上?
1 | Number.prototype.say = function () { |
==这是因为.
运算符提供了封箱操作,它会根据基本类型构造一个临时对象,使得我们能够在基本类型上调用对应的对象的方法。==
类型转换
封箱转换
把基本类型转换为对应的对象,装箱独享都有私有的Class
属性,可以用Object.prototype.toString
来获取。
JavaScript中没有任何办法可以更改私有的class
属性,因此Object.prototype.toString
是可以准确识别对象对应的基本类型的方法
拆箱转换
ToPrimitive
函数是对象类型到基本类型的转换,对象到String和Number的转换都遵循“先拆箱再转换”的规则,拆箱转换会调用valueOf
和toString
类获得拆箱后的基本类型
转换为数字时首先调用valueOf
,转换为字符串时首先调用toString
ES6中可以通过显示指定Symbol.toPrimitive
来覆盖原有行为
06 | JavaScript对象:面向对象还是基于对象
JavaScript中的对象与其他语言的对象有一些不同之处,例如:
- JavaScript在ES6以前没有类的概念
- JavaScript对象可以自由添加属性,其他语言不能
到底什么是面向对象?
面向对象是顺着人类思维模式产生的一种抽象,从人类认知角度,对象应该是下列事物之一:
- 一个可以触摸或者可以看见的东西
- 人的智力可以理解的东西
- 可以指导思考或行动(进行想象或施加动作)的东西
JavaScript没有采用C++、Java等流行的编程语言采用的“类”的方式来描述对象,而是采用了一个更为冷门的方式“原型”来描述对象
但是由于公司正值原因,JavaScript推出时在“原型运行时”的基础上增加了new
、this
的特性,使之看起来更像Java。
对象的特点
对象有如下的特点:
(1)对象具有唯一标识性:即使完全相同的两个对象,也并非一个同一个对象
对象的唯一标识性是用内存地址来体现的,对象具有唯一标识的内存地址
1 | var o1 = { a: 1 }; |
(2)对象有状态:对象具有状态,同一个对象可能处于不同状态之下
(3)对象具有行为:对象的状态,可能因为它的行为产生变迁
第二个和第三个特征“状态”和行为,在JavaScript中统一抽象为“属性”(因为JavaScript中函数也是一个特殊的对象)
JavaScript在实现对象基本特征的基础上,JavaScript对象独有的特色是:==对象具有高度的动态性,这是因为JavaScript赋予了使用者在运行时为对象添改状态和行为的能力==。
JavaScript对象的两类属性
JavaScript对象的属性并非只是简单的键值对,而是用一组特征来描述属性
第一类是数据属性(descriptor),有四个特征:value
/writable
/enumerable
/configurable
第二类是访问器属性,也有四个特征(setter
/getter
/enumerable
/configruable
访问器属性使得使用者在读和写属性时,可以执行代码得到不同的值,可以视为一种函数的语法糖
通常定义属性的代码会产生数据属性的value
特征值,其他的数据属性的特征值默认为true
,使用Object.getOwnPropertyDescripter
可以查看数据属性特征值,使用Object.defineProperty
可以改变数据属性特征值和访问器属性
在创建对象时,可以使用get
和set
关键字阿里创建访问器属性:
1 | var o = { |
对象是一个属性的索引结构,key是属性名,属性值是一系列描述属性的集合
1 | { |
JavaScript提供了完全运行时的对象系统,可以模仿多数面向对象的编程范式,比如基于类和基于原型,但是由于它的对象系统是独特的、具有高度动态性的对象系统,这让它与其他面向对象的语言不通
07 | JavaScript对象:我们真的需要模拟类吗
早期JavaScript的模拟面向对象,实际上是模拟基于类的面向对象
而JavaScript的面向对象本质上是基于原型的,而由于历史原因,不得不加入了new
、this
等语言特性来模拟类
JavaScript的原型复制操作采用了引用的思路:一个对象并不是真的去复制一个原型对象,而是使得新对象持有一个原型的引用
原型系统
用两条概括原型系统
- 对象通过私有字段
[[proto]]
连接到其原型 - 读取一个属性,如果对象本身没有,则会继续访问对象的原型,知道原型为空或者找到为止
ES6提供了一些列内置函数,可以更方便的访问、操作原型,使用这三个方法,可以完全抛开类的模拟,直接利用原型来实现抽象、继承和复用。这三个方法是:
Object.create(a, b)
,根据指定的原型(a
)创建新对象,原型可以是null
,b
是新对象的描述属性(数据属性)Object.getPrototypeOf
获得一个对象的原型(等同于直接获取对象的__proto__
属性)Object.setPrototypeOf
设置一个对象的原型(等同于直接设置对象的__proto__
属性)
用构造函数模拟类
我们现在用来判断类型的最准确的方法Object.prototype.toString
获得的结果[object xxxx]
,实际上就是早期版本的“类”的概念,它原本是一个私有属性[[class]]
,从ES5开始,[[class]]
被Symbol.toStringTag
定义,而Object.prototype.toString
的结果可以使用Symbol.toStringTag
定义:
1 | var o = { [Symbol.toStringTag]: "MyObject" }; |
用构造函数模拟类,关键就是使用new
运算符,当我们对一个构造函数执行了new
操作,实际上发生了一下几件事:
1 | let p = new Person(); |
new
运算的返回值默认返回this
,但显式的返回值时,如果返回值的是==基本类型==,则忽略返回值,仍然返回this
,如果返回值是==引用类型==,则直接返回该返回值作为对象的结果
new
的行为,视图让函数对象在语法上跟类变得相似,它客观上提供了两种范式,一种是在构造器中的this
上添加实例属性,另外一种是在构造器的prototype
原型对象上添加原型属性
在早期没有Object.create
等方法的早期版本中,new
是唯一一个可以指定[[proto]]
的方法(直接访问__proto__
是不被推荐在生产环境使用的)
下面是Object.create
简单的Polpyfill:
1 | function create (prototype) { |
ES6中的类class
ES6中的class
用一种看起来更符合主流意义上的类的形式来模拟JS中的类,但本质上还是利用原型系统实现继承和复用,只不过用class
代替了new
和function
的搭配,看起来更协调而已。
推荐使用ES6的语法来定义类,令function
回归原本的函数语义。
ES6中引入的class
关键字,意味着类的概念从属性升级为语言的基础设施,从此,基于类的编程方式成为了JavaScript的官方编程范式。
在类中通过get
/set
关键字创建getter
/setter
,类中定义的方法和属性会被写在原型对象上,数据型成员最好写在构造器里面:
1 | class Rectangle { |
类的写法实际上也是由原型运行时来承载的(我认为是原型系统的一种语法糖),逻辑上JavaScript认为每个类是有共同原型的一组对象。
类通过extends
提供了继承能力:
1 | class Animal { |
比起早期的继承方式,extends
关键字自动设置了constructor
,并且会自动调用父类的构造函数,是一种更少坑的设计。
所以当我们使用类的思想来设计代码时,应该尽量使用class
来声明类,而不是用旧语法,拿函数类模拟对象
(但是我认为,可以使用新的语法,但是如果不搞清楚本质还是不行的,需要弄清楚JavaScript实现继承和复用的根本原理,搞清楚JavaScript中原型链的原理,这是根本)
(这一节的内容实际上信息量很大,讲述的是JavaScript中最基础、最精华的部分,设计能力的提高离不开这部分基础,之前自己总结过相关的笔记《Javascript面向对象编程2–构造函数的继承》,需要反复的思考和复习,打好基础)
08 | JavaScript对象:你知道全部的对象分类吗?
这两天,offer的事情有了着落,又刚过完年回来,工作又闲的冒泡,自我驱动能力大幅下降,有点控制不住自己了
关于这一章的内容,实际上是比较复杂且深奥的,需要好多储备知识才能大概理解,而前几章我一直有一个不太满意的点,就是感觉作者的思路,或者说是语言组织,有一种故作高深的感觉,把简单的东西搞的很复杂的样子,把不好理解的知识用一些更不好理解的语言讲解,让人理解起来更加费劲,而且思路常常碎片化。
比较之下,更显得阮一峰的难得,举重若轻,平易近人,这才是我学习的榜样。
可以把对象分成几类:
(1)==宿主对象==,有JavaScript宿主环境提供的对象,它们的行为完全由宿主环境决定
(2)==内置对象==,有JavaScript语言提供的对象,包括:
- 固有对象,有标准规定,随着JavaScript运行时创建而自动创建的对象实例
- 原生对象,可以由用户通过Array,RegExp等内置构造器或者特殊语法创建的对象
- 普通对象,由
{}
、Object
构造器或者class
关键字创建的对象,能够被原型继承
宿主对象
JavaScript中常见的宿主对象就是浏览器提供的宿主对象,在这个宿主对象中,全局对象是window
,window
上有很多属性,比如document
实际上,window
对象上的属性一部分来自浏览器环境,另外一部分来自JavaScript语言(JavaScript的标准中规定了全局对象属性)
宿主对象也分为固有的和用户可创建的两种,比如document.createElement
就可以创建一些dom
对象
宿主也会提供一些构造器,比如可以使用new Image
创建<img>
元素
内置对象·固有对象
固有对象在任何JavaScript代码执行前就已经被创建出来了,它们通常扮演者饿类似基础库角色,ECMA中规定的固有对象有150+个
内置对象·原生对象
我们将JavaScript中,能够通过语言本身的构造器创建的对象作为原生对象。JavaScript提供了30多个构造器,可以分成以下几类:
可以使用new
运算符调用构造器创建新的对象,这些构造器的能力是原生的(即无法用纯JavaScript代码实现的),也无法用class
/extend
来继承的
这些构造器创建的对象多数使用了私有字段,例如Error: [[ErrorData]]
、Boolean: [[BooleanData]]
等,这些字段使得原型继承方法无法正常工作,所以可以认为,这些原生对象都是为了特定能力或性能,而设计出来的“特权对象”
用对象来模拟函数与构造器:函数对象和构造器对象
JavaScript为用来模拟函数与构造器的对象预留了私有字段,并据此规定了抽象的函数对象和构造器对象的概念
函数对象的定义是,具有[[call]]
私有字段的对象
构造器对象的定义是,具有[[construct]]
私有字段的对象
JavaScript用对象模拟函数的设计代替了一般的编程语言中的函数,它们可以像其他语言的函数一样被调用、传参。任何宿主,只要提供了“具有[[call]]
私有字段的对象”,就可以被JavaScript的函数调用的语法锁调用
[[call]]
私有字段必须是一个引擎中定义的函数,需要接受this
值和调用参数,并且会产生域的切换。
任何对象只需要实现[[call]]
,它就是函数对象,可以作为函数被调用。如果它能够实现[[construct]]
,它就是构造器对象,可以作为构造器被调用。
宿主对象和内置对象的表现
对于宿主对象和内置对象来说,他们实现[[call]]
(作为函数被调用)和[[construct]]
(作为构造器调用)也不总是一致。比如内置对象Date
作为构造器调用时产生新的对象,作为函数时则返回字符串
1 | typeof Date() |
在浏览器宿主环境中提供的Image
构造器,则根本不允许被作为函数调用:
1 | new Image() |
基本类型(String
、Number
、Boolean
)作为构造器时返回对象,作为函数调用时则产生类型转换的效果。
==注意的是,ES6的箭头函数创建的仅仅是函数,而不能作为构造器使用。==
使用function
创建的函数的行为
当使用function
关键字或者使用Function
创建的函数必定同时是函数和构造器,它们执行同一段代码,但是表现出来的行为效果却不相同。
以前遇到过面试题,题目的中心思想就是如何判断一个函数是作为构造函数调用,还是作为普通函数被调用。两种行为的结果是不同的,进行判断的话就需要搞清楚new
调用构造函数时发生了什么:
- 创建一个新的对象
{}
- 实现原型链的继承
- 将构造函数的作用域赋给新对象(因此
this
对象就指向了新对象) - 执行构造函数的代码
- 返回新对象(构造函数调用默认返回
this
,但显式的返回值时,如果返回值的是基本类型,则忽略返回值,仍然返回this
,如果返回值是引用类型,则直接返回该返回值作为对象的结果)
所以要判断我认为只需要在函数内部判断this
是否是该函数的实例即可。
对象的特殊行为
在固有对象和原生对象中,有一些对象的默写行为与正常对象有很大区别,主要是下标运算(即使用中括号或者.
来做属性访问)和设置原型继承
主要有:
Array
:Array
的length
属性根据最大的下标自动发生变化String
:String
的正整数属性访问会去字符串里查找arguments
:arguments
的非负整数型下标跟对其函数父对象的输入的变量联动Object.prototype
:作为所有正常对象的默认原型,不能在设置原型了(这就是语言的规定,Object.prototype.__proto__ === null
,没有为什么)- 模块的
namespace
对象:特殊的地方非常多,跟一般对象完全不用了
一个练习
不使用new
运算符,极可能找到获得对象的方法
1 | let o = {}; |