重学前端

370 阅读21分钟

01 明确学习路线和方法

建立知识架构

一种是 html/css/js(但是除了这些语言 还有打包/测试/dom和bom等等问题) 一种是 文法/语义/运行时(粗略视为语法/语法的意义/算法和数据结构)

追本溯源

了解背景

02 前端知识架构图

写在另一篇 juejin.cn/post/684490…
前端架构的主要职责是兼容性、复用和能力扩展

03/04语义化

写在上面那一篇 juejin.cn/post/684490…
语义化就像写论文,规规矩矩遵守格式

05 js类型

写在上面那一篇 juejin.cn/post/684490…
js中的undefined是一个变量 而不是一个关键字 是js公认的设计失误 但是现在最新谷歌浏览器已经不能更新undefined的值了

06 面向对象还是基于对象

相对于其他语言
为什么javascript有对象的概念 但没有类的概念
为什么javscript在运行过程中可以更改对象的属性
为什么javascript可以随意添加任何属性

c++,java是使用类的方式来描述对象,但javascript选择了原型,并在原型运行时的基础上引入了new,this等语言特性,使javascript看起来像java,但其实实现原理完全不同

对象: 一切事物的总称.
在《面向对象分析与设计》这本书中,Grady Booch替我们做了总结,他认为,从人类的认知角度来说,对象应该是下列事物之一: 一个可以触摸或者可以看见的东西;
人的智力可以理解的东西;
可以指导思考或行动(进行想象或施加动作)的东西。

对象有以下特征:

  1. 对象具有唯一标识性:即使完全相同的两个对象,也并非同一个对象。
    => 地址不同,不管内容如何,都不是同一个对象.对象具有唯一地址标识
var o1 = { a: 1 };
    var o2 = { a: 1 };
    console.log(o1 == o2); // false
  1. 对象有状态:对象具有状态,同一对象可能处于不同状态之下。
    => 对象可以有自己的属性,并且同一个对象,属性可能不同
  2. 对象具有行为:即对象的状态,可能因为它的行为产生变迁。
    => 在调用对象方法的过程中,可以更改对象的属性
var o = { 
        d: 1,
        f() {
            console.log(this.d);
        }    
    };

d和f虽然写法不太相同,但对于javascript来说,d和f就是两个普通属性
javascript允许运行时向对象添加属性,这跟绝大多数鲫鱼类/静态的对象设计完全不同

var o = { a: 1 };
    o.b = 2;
    console.log(o.a, o.b); //1 2

对象的两类属性

  1. 数据属性 的特征
    value:就是属性的值。
    writable:决定属性能否被赋值。
    enumerable:决定for in能否枚举该属性。
    configurable:决定该属性能否被删除或者改变特征值。
    大多数情况下我们只关心数据属性的值就可以

  2. 访问器属性(getter/setter) 的特征
    getter:函数或undefined,在取属性值时被调用。
    setter:函数或undefined,在设置属性值时被调用。
    enumerable:决定for in能否枚举该属性。
    configurable:决定该属性能否被删除或者改变特征值。
    以上值都默认为true
    可以用Object.getOwnPropertyDescripter来查看

var o = {a:1}
o.b = 2
Object.getOwnPropertyDescriptor(o,"a")
//{value: 1, writable: true, enumerable: true, configurable: true}

想要改变属性的特征或者定义访问器属性,可以用Object.defineProperty

var o = { a: 1 };
Object.defineProperty(o, "b", {value: 2, writable: false, enumerable: false, configurable: true});
//{a: 1, b: 2}
Object.getOwnPropertyDescriptor(o,"a"); 
//{value: 1, writable: true, enumerable: true, configurable: true}
 Object.getOwnPropertyDescriptor(o,"b");
//{value: 2, writable: false, enumerable: false, configurable: true}
 o.b = 3;
console.log(o.b); // 2

如果我们改变一个对象的get的方法,当我们访问这个对象时 就会变成我们改写的样式

 var o = { get a() { return 1 } };

    console.log(o.a); // 1

所以javascript对象的运行时就是一个"属性的集合",属性一字符串或symbol为key,以数据属性特征值或者访问器属性特征值为value

总结: javascript和其他基于类的面向对象语言有比较大的差异

  1. javascript中只有对象的概念没有类的概念
  2. javascript对象在运行时过程中可以随意修改属性
  3. javascript对象可以添加任何种类的属性
  4. 以上两点可以总结为 对象具有高度的动态性 这也是javascript的具体设计

但javascript也是面向对象的,因为
面向对象: 使用对象时,只关注对象提供的功能,不关注内部细节 (没有太理解 待续)

我们真的需要模拟类吗

js如果模拟基于类的面向对象,能怎么实现呢?

“基于类”的编程提倡使用一个关注分类和类之间关系开发模型。在这类语言中,总是先有类,再从类去实例化一个对象。类与类之间又可能会形成继承、组合等关系。类又往往与语言的类型系统整合,形成一定编译时的能力。
与此相对,“基于原型”的编程看起来更为提倡程序员去关注一系列对象实例的行为,而后才去关心如何将这些对象,划分到最近的使用方式相似的原型对象,而不是将它们分成类。

javascript这样的半吊子模拟,缺少了继承/多态等关键特性,导致大家试图对它进行修补,从而产生了种种互不相容的解决方案.
从es6开始,js提供了class关键词来定义类

原型:

  1. 如果所有对象都有私有字段[[prototype]],就是对象的原型
  2. 读一个属性,如果对象本身没有,则会继续访问对象的原型,直到原型为空,或者找到为止

Object.create 根据指定的原型创建新对象,原型可以是null; Object.getPrototypeOf 获得一个对象的原型; Object.setPrototypeOf 设置一个对象的原型。

模拟继承

var cat = {
    say(){
        console.log('meow~');
    },
    jump(){
        console.log('jump');
    }
}

var tiger = Object.create(cat,{
    say:{
        value:function(){
            console.log('roar!');
        }
    }
})

var anotherCat = Object.create(cat);
anotherCat.say(); //meow~

var anotherTiger = Object.create(tiger);
anotherTiger.say(); //roar!

或者用prototype在原型上处理

function c1() {
    this.p1 = 1;
    this.p2 = function () {
        console.log(this.p1);
    }
}
var o1 = new c1;
o1.p2();

function c2() {

}
c2.prototype.p1 = 2;
c2.prototype.p2 = function () {
    console.log(this.p1);
}
var o2 = new c2();
o2.p2();

在es6中 引入了class和继承

class Rectangle{
    constructor(height,width){
        this.height = height;
        this.width = width;
    }
    area(){
        return this.calcArea();
    }
    get area2(){
        return this.calcArea();
    }
    set area2(value){
        this.height = value
    }
    calcArea(){
        return this.height * this.width;
    }
}

let shape = new Rectangle(2,3);
console.log(shape.area())
shape.area2 = 5;
console.log(shape.area2)

class square extends Rectangle{
    constructor(height,width){
        super(height,width);
    }
    set area2(value){
        this.height = value*2;
    }
}
let shape2 = new square(2,3);
console.log(shape2.area())
shape2.area2 = 5;
console.log(shape2.area2)

你知道全部的对象分类吗

js可以分为几类 - 宿主对象(比如浏览器环境中的window)和内置对象
内置对象又分为

  1. 固有对象 - 由标准规定,随着js运行时创建而自动创建的对象实例
  2. 原生对象 - 可以由用户通过Array/RegExp等内置构造器或者特殊语法创建的对象

这些方法无法被继承,所有这些原生对象都是为了特定能力或者性能,而设计出来的“特权对象”。
3. 普通对象 - 由{}语法/Object构造器或者class关键字定义类创建的对象,能够被原型继承

函数对象的定义是:具有[[call]]私有字段的对象,构造器对象的定义是:具有私有字段[[construct]]的对象。
任何对象只需要实现[[call]],它就是一个函数对象,可以去作为函数被调用。而如果它能实现[[construct]],它就是一个构造器对象,可以作为构造器被调用。
用户用function关键字穿件的函数必定同时是函数和构造器,但他们表现出来的行为效果却不相同

console.log(typeof new Date);  //object 作为构造器被调用
console.log(typeof Date());  //string 作为函数被调用
console.log(typeof Date); //function 
function f(){
    return 1;
}
var v = f(); //把f作为函数调用
var o = new f(); //把f作为构造器调用

我们可以大致认为,construct的执行过程如下 以 Object.protoype 为原型创建一个新对象;
以新对象为 this,执行函数的[[call]];
如果[[call]]的返回值是对象,那么,返回这个对象,否则返回第一步创建的新对象。

这时如果我们的构造器返回了一个新的对象,那么我们可以在一定程度上实现私有

function cls(){
    this.a = 100;
    return {
        getValue:() => this.a
    }
}
var o = new cls;
o.getValue(); //100
//a在外面永远无法访问到

带@的规则

css的顶层样式表由两种规则组成的规则列表构成,一种是at规则,另一种是普通规则

  1. @charset
    @charset用于提示CSS文件使用的字符编码方式,它如果被使用,必须出现在最前面。这个规则只在给出语法解析阶段前使用,并不影响页面上的展示效果。
@charset "utf-8";
  1. @import
    @import用于引入一个CSS文件,除了@charset规则不会被引入,@import可以引入另一个文件的全部内容。
@import "mystyle.css";
@import url("mystyle.css");
@import [ <url> | <string> ]
        [ supports( [ <supports-condition> | <declaration> ] ) ]?
        <media-query-list>? ;
  1. @media
    media就是大名鼎鼎的media query使用的规则了,它能够对设备的类型进行一些判断。在media的区块内,是普通规则列表。
@media (min-width:800px){
    body { font-size: 10pt }
}

@media一般用来判断宽度不同,从而表现出不同的样式,但其实判断条件还有很多
width | min-width | max-width
height | min-height | max-height
device-width | min-device-width | max-device-width device-height | min-device-height | max-device-height aspect-ratio(宽高比) | min-aspect-ratio | max-aspect-ratio
device-aspect-ratio(设备宽高比) | min-device-aspect-ratio | max-device-aspect-ratio
color | min-color | max-color
color-index(颜色索引) | min-color-index | max-color-index
monochrome(黑白) | min-monochrome | max-monochrome
resolution(分辨率) | min-resolution | max-resolution
scan(扫描) | grid(网格设备还是位图设备)|orientation(方向)
4. @page
page用于分页媒体访问网页时的表现设置,页面是一种特殊的盒模型结构,除了页面本身,还可以设置它周围的盒。

@page {
  size: 8.5in 11in;
  margin: 10%;

  @top-left {
    content: "Hamlet";
  }
  @top-right {
    content: "Page " counter(page);
  }
}
  1. @ counter-style
    counter-style产生一种数据,用于定义列表项的表现。
@counter-style triangle {
  system: cyclic;
  symbols: ‣;
  suffix: " ";
}
  1. @ key-frames
keyframes产生一种数据,用于定义动画关键帧。
  1. @ font-face
    fontface用于定义一种字体,icon font技术就是利用这个特性来实现的。
@font-face {
  font-family: Gentium;
  src: url(http://example.com/fonts/Gentium.woff);
}

p { font-family: Gentium, serif; }
  1. @ support
    support检查环境的特性,它与media比较类似。
  2. @ namespace 用于跟XML命名空间配合的一个规则,表示内部的CSS选择器全都带上特定命名空间。
  3. @ viewport 用于设置视口的一些特性,不过兼容性目前不是很好,多数时候被html的meta代替。
  4. 其它
    除了以上这些,还有些目前不太推荐使用的at规则。 @color-profile 是 SVG1.0 引入的CSS特性,但是实现状况不怎么好。
    @document 还没讨论清楚,被推迟到了CSS4中。
    @font-feature-values 。todo查一下。

普通规则
我们从语法结构可以看出,任何选择器,都是由几个符号结构连接的:空格、大于号、加号、波浪线、双竖线,这里需要注意一下,空格,即为后代选择器的优先级较低。
CSS支持一批特定的计算型函数:

  1. calc()
    calc()函数允许不同单位混合运算
    (less中calc要写成calc(~"100% - 30px"),有变量的话写成calc(~"100% - @{diff}")
  2. max()
    max()表示取两数中较大的一个
  3. min()
    min()表示取两数之中较小的一个
  4. clamp()
    clamp()则是给一个值限定一个范围,超出范围外则使用范围的最大或者最小值。
  5. toggle()
    toggle()函数在规则选中多于一个元素时生效,它会在几个值之间来回切换,比如我们要让一个列表项的样式圆点和方点间隔出现,可以使用下面代码:(貌似浏览器不太支持)
ul { list-style-type: toggle(circle, square); }
  1. attr()
    attr()函数允许CSS接受属性值的控制。

一个浏览器是如何工作的

  1. 浏览器首先使用HTTP协议或者HTTPS协议,向服务端请求页面;
  2. 把请求回来的HTML代码经过解析,构建成DOM树;
  3. 计算DOM树上的CSS属性;
  4. 最后根据CSS属性对元素逐个进行渲染,得到内存中的位图;
  5. 一个可选的步骤是对位图进行合成,这会极大地增加后续绘制的速度;
  6. 合成之后,再绘制到界面上。

HTTP请求回来之后这个过程并不是上一步完全处理完后再进行下一步,而是流水线般,处理一部分输出一部分

  1. HTTP协议 HTTP协议是基于TCP协议出现的,对TCP协议来说,TCP协议是一条双向的通讯通道,HTTP在TCP的基础上,规定了Request-Response模式,这个模式决定了通讯必定是由浏览器端首先发起的.
    大部分情况下,浏览器的实现者只需要一个TCP库,甚至一个现成的HTTP库就可以搞定浏览器的网络通讯部分,http是纯粹的文本协议,它是规定了使用TCP协议来传输文本格式的一个应用层协议

请求部分

GET / HTTP/1.1
Host: time.geekbang.org

回应部分

HTTP/1.1 301 Moved Permanently
Date: Fri, 25 Jan 2019 13:28:12 GMT
Content-Type: text/html
Content-Length: 182
Connection: keep-alive
Location: https://time.geekbang.org/
Strict-Transport-Security: max-age=15768000

<html>
<head><title>301 Moved Permanently</title></head>
<body bgcolor="white">
<center><h1>301 Moved Permanently</h1></center>
<hr><center>openresty</center>
</body>
</html>

这就是一次完整的HTTP请求的过程了,我们可以看到,在TCP通道中传输的,完全是文本。
在请求部分,第一行被称作 request line,它分为三个部分,HTTP Method,也就是请求的“方法”,请求的路径和请求的协议和版本。
在响应部分,第一行被称作 response line,它也分为三个部分,协议和版本、状态码和状态文本。
紧随在request line或者response line之后,是请求头/响应头,这些头由若干行组成,每行是用冒号分隔的名称和值。
在头之后,以一个空行(两个换行符)为分隔,是请求体/响应体,请求体可能包含文件或者表单数据,响应体则是html代码。

  1. Method
    方法有以下几种定义:
  • GET
  • 浏览器通过地址栏访问页面都是GET方法。表单提交产生POST方法。
  • POST
  • HEAD
  • HEAD则是跟GET类似,只返回请求头,多数由JavaScript发起
  • PUT
  • DELETE
  • PUT和DELETE分别表示添加资源和删除资源,但是实际上这只是语义上的一种约定,并没有强约束。
  • CONNECT
  • CONNECT现在多用于HTTPS和WebSocket。
  • OPTIONS
  • OPTIONS和TRACE一般用于调试,多数线上服务都不支持。
  • TRACE
  1. 状态码
  • 1xx 临时回应,表示客户端请继续(前端一般看不见 因为1xx的状态会被浏览器直接处理掉 不会发送到上层应用)
  • 2xx 请求成功(200)
  • 3xx 表示请求的目标有变化,希望客户端进一步处理
    301&302 永久性和临时性跳转 (重定向) 304 客户端缓存没有更新 (当浏览器通过实践或tag判断没有更新时 会返回一个不含body的304状态)
  • 4xx 客户端请求错误
    403 无权限
    404 请求的页面不存在
  • 5xx 服务端请求错误
    500 服务端错误
    503 服务端暂时性错误 可以稍后再试
  1. HTTP头
    请求头
    回应头

HTTP Request Body HTTP请求的body主要用于提交表单场景。实际上,http请求的body是比较自由的,只要浏览器端发送的body服务端认可就可以了。一些常见的body格式是:

application/json
application/x-www-form-urlencoded
multipart/form-data
text/xml
我们使用html的form标签提交产生的html请求,默认会产生 application/x-www-form-urlencoded 的数据格式,当有文件上传时,则会使用multipart/form-data。

HTTPS
HTTPS有两个作用,一是确定请求的目标服务端身份,二是保证传输的数据不会被网络中间节点窃听或篡改
实际上,他只是对传输的内容做一次加密,从传输内容上看,HTTPS HTTP没有任何区别

HTTP2 HTTP2是HTTP1.1的升级版本,你可以查看它的详情链接。

tools.ietf.org/html/rfc754…

HTTP2.0
最大的改进有两点,一是支持服务端推送、二是支持TCP连接复用。

服务端推送能够在客户端发送第一个请求到服务端时,提前把一部分内容推送给客户端,放入缓存当中,这可以避免客户端请求顺序带来的并行度不高,从而导致的性能问题。

TCP连接复用,则使用同一个TCP连接来传输多个HTTP请求,避免了TCP连接建立时的三次握手开销,和初建TCP连接时传输窗口小的问题。

Note: 其实很多优化涉及更下层的协议。IP层的分包情况,和物理层的建连时间是需要被考虑的。

一个浏览器是如何工作的(二)

我们了解了浏览器使用HTTP协议或者HTTPS协议,向服务端请求页面的过程
现在我们来看如何解析请求回来的HTML代码

一个标签是怎么被拆分的

<p class="a">text text text</p>

被拆分为

1.<p   2.class="a"   3.>   4.text text text   5.</p>

在接受第一个字符之前,我们完全无法判断这是哪一个词,但是随着我们接受的字符越来越多,拼出其他的内容可能性就越来越少
这里我们为了理解原理,用这个简单的状态机就足够说明问题了。
状态机的初始状态,我们仅仅区分 “< ”和 “非<”:
如果获得的是一个非<字符,那么可以认为进入了一个文本节点; 如果获得的是一个<字符,那么进入一个标签状态。 不过当我们在标签状态时,则会面临着一些可能性。
比如下一个字符是“ ! ” ,那么很可能是进入了注释节点或者CDATA节点。
如果下一个字符是 “/ ”,那么可以确定进入了一个结束标签。
如果下一个字符是字母,那么可以确定进入了一个开始标签。
如果我们要完整处理各种HTML标准中定义的东西,那么还要考虑“ ? ”“% ”等内容。
我们可以看到,用状态机做词法分析,其实正是把每个词的“特征字符”逐个拆开成独立状态,然后再把所有词的特征字符链合并起来,形成一个联通图结构。

  • 栈顶元素就是当前节点;
  • 遇到属性,就添加到当前节点;
  • 遇到文本节点,如果当前节点是文本节点,则跟文本节点合并,否则入栈成为当前节点的子节点;
  • 遇到注释节点,作为当前节点的子节点;
  • 遇到tag start就入栈一个节点,当前节点就是这个节点的父节点;
  • 遇到tag end就出栈一个节点(还可以检查是否匹配)。

一个浏览器是如何工作的(三)

浏览器会尽量流式处理整个过程,而dom的构建过程是:从父到子,从先到后,一个一个节点构造,并且挂载到dom树上的,那么这个过程中,css属性什么什么时候计算出来的呢?
结果是同步计算的.
在渲染的过程中,我们依次拿到上一步构建好的元素,去检查它匹配到了哪些规则,再根据规则的优先级,做覆盖和调整
选择器的出现顺序,必定和构建dom树的顺序一致,这是一个css设计的原则,即保证选择器在dom树构建到当前节点时,已经可以准确判断是否匹配,不需要后续节点信息
这也就是 为什么不会出现父元素选择器,因为是一层一层构建的

一个浏览器是如何工作的(四)

我们已经给dom元素添加了用于展现的css属性,接下来,浏览器的工作就是确定每一个元素的位置,我们的原则依然是尽可能流式处理上一步骤的输出
在构建dom树和及计算css属性这两个步骤时,我们的产出都是一个一个的元素,但是在排版这个步骤中,在某些情况下,我们就不能这么做了,尤其是表格相关排版,flex排版和grid排版,它们有一个显著的特点,就是子元素之间具有关联性

一个浏览器是如何工作中的(五)

最后,我们根据这些样式信息和大小信息,为每个元素在内存中渲染它的图形,并把它绘制到对应的位置
渲染(render).就是把模型变成位图的过程,这里的位图就是在内存中建立一张二维表格,把一张图片的每个像素对应的颜色保存进去(位图信息也是dom树中占据浏览器内存最多的信息,我们在做内存优化时,主要是考虑这一部分)

  • 渲染
    浏览器中渲染这个过程,就是把每一个元素对应的盒变成位图,这里的元素包括HTML元素和伪元素,一个元素可能对应多个盒(比如说inline元素,可能会对应多个元素),每一个盒对应一张位图
    这个渲染过程是非常复杂的,但是大体上可以分为两个大类:图形和文字
    盒的背景/边框/svg元素/阴影等特性,都是需要绘制的图形类,这就像我们实现HTTP协议必须要基于TCP库一样,这一部分,我们需要一个底层库来支持
    盒中的文字,也需要底层库来支持,叫做字体库

  • 合成
    合成就是为一些元素创建一个合成后的位图,把一部分子元素渲染到合成的位图上面
    那么这个策略是怎样的呢,合成是一个性能考量,那么这个合成的目标就是提高性能,根据这个目标,我们建立的原则就是最大限度地减少绘制次数原则
    如果我们把所有元素进行合成,那么一旦我们用js改变了任何一个css属性,这份合成后的位图就失效了,我们需要重新绘制所有的元素
    那么如果我们所有的元素都不合成,会怎么样呢?结果就是,相当于我们每次都必须重新绘制所有的元素,这也不是对性能友好的选择
    那么好的合成策略是什么呢?好的合成策略是猜测可能变化的元素,把它排除到合成之外
    目前,主流浏览器一般根据position,transform等属性来决定合成策略,来猜测这些元素未来可能会发生变化
    新的css标准中,规定了will-change属性,可以由业务代码来提示浏览器的合成策略,灵活运用这样的特性,可以大大提升合成策略的效果

  • 绘制
    绘制是把“位图最终绘制到屏幕上,变成肉眼可见的图像”的过程,不过,一般来说,浏览器并不需要用代码来处理这个过程,浏览器只需要把最终要显示的位图交给操作系统即可。
    一般最终显式的位图位于显存中,也有一些情况下,浏览器只需要把内存中的一张位图提交给操作系统或者显式驱动就可以了,这取决于浏览器运行的环境。不过无论如何,我们把任何位图合成到这个“最终位图”的操作称为绘制。

  • 总结
    在这一节课程中,我们讲解了浏览器中的位图操作部分,这包括了渲染、合成和绘制三个部分。渲染过程把元素变成位图,合成把一部分位图变成合成层,最终的绘制过程把合成层显示到屏幕上。

当绘制完成时,就完成了浏览器的最终任务,把一个URL最后变成了一个可以看的网页图像。当然了,我们对每一个部分的讲解,都省略了大量的细节,比如我们今天讲到的绘制,就有意地无视了滚动区域.