名谓扫盲,实则是为自己扫盲。前些日子通过Elm的学习接触到了函数式编程的概念,发现语言风格和以C为代表的命令式编程大不相同,接触不同的编程思维还是很有助于自我提升的。在回顾的同时,这里走马观花地带过一些函数式编程的“热门词汇”。

历史故事

什么是函数式编程(Functional Programming,FP)?它从何而来?可以吃吗?这得从20世纪30年代开始讲起:

新建成的哥特式办公楼给普林斯顿大学带来一种天堂般的安全感。来自世界各地的逻辑学者应邀来到普林斯顿,他们将组建一个新的学部。正当大部分美国人还在为找不到一片面包做晚餐而发愁的时候,在普林斯顿却是这样一番景象:高高的天花板和木雕包覆的墙,每天品茶论道,漫步丛林。 一个名叫阿隆佐·邱奇(Alonzo Church)的年轻数学家就过着这样优越的生活。阿隆佐本科毕业于普林斯顿后被留在研究院。他觉得这样的生活完全没有必要,于是他鲜少出现在那些数学茶会中也不喜欢到树林里散心。阿隆佐更喜欢独处:自己一个人的时候他的工作效率更高。尽管如此他还是和普林斯顿学者保持着联系,这些人当中有艾伦·图灵约翰·冯·诺伊曼库尔特·哥德尔

在与这些人的合作下,阿隆佐设计了一个名为lambda演算的形式系统。在这种语言里面,函数的参数是函数,返回值也是函数。篇幅和本人能力限制,不对lambda演算做更多讲解。

除了阿隆佐·邱奇,艾伦·图灵也在进行类似的研究。他设计了一种完全不同的系统(后来被称为图灵机),并用这种系统得出了和阿隆佐相似的答案。到了后来人们证明了图灵机和lambda演算的能力是一样的。

到了50年代末,一个叫John McCarthy的MIT教授(他也是普林斯顿的硕士)对阿隆佐的成果产生了兴趣。1958年他发明了一种列表处理语言(Lisp),这种语言是一种阿隆佐lambda演算在现实世界的实现,而且它能在冯·诺伊曼计算机上运行!而后的诸多函数式编程语言(如Haskell,ML等)也多少收到Lisp的影响。

法则

函数式编程的思想来源Lambda演算在最初设计时就是用来解决计算相关问题,它是一种相对于“命令式编程”完全不同的编程范式,后者告诉计算机怎么做,前者着眼在从数学角度描述问题。它的特点也很明显:

  • 变量不可变,即默认带上const或是final(当然函数式编程里压根没有constfinal的概念)。这么来看,叫它为“符号”似乎更合适
  • 惰性求值,变量直到使用时才会真正计算它的值,因为这个特点,Haskell甚至允许无限列表的出现。同时,这也意味着语句的出现顺序和执行顺序并不相关。
  • 高阶函数,函数可以作为入参或是返回值,这个也被很多不那么OOP的语言借鉴去了
  • 无副作用函数只负责映射数据,更像是个管道,绝不改变外部状态,同样的输入在任何时候会得到同样的输出(测试人员笑开了花)。这一点使得函数式编程语言天生支持并发执行。
  • 一切皆函数,函数是第一公民

λ演算用来描述一种形式系统,它的语法只有三条:

语法 术语 描述
a 变量 一个代表参数或数字/逻辑值的符号或字符串
(λx.M) 定义 函数定义,.前面的标识符x为入参,M为表达式
(M N) 调用 应用函数到一个入参

例如:((λ x y. x + y) 1 2)表示1和2相加。

λ演算公理只有两个:

操作 名称 描述
(λx.M[x]) → (λy.M[y]) α变换 改变入参名不影响结果
((λx.M) E) → (M[x:=E]) β规约 将入参传入λ意味着对它做演算

还以上面的相加为例,α变换就是λ x y. x + y → λ a b. a + b;β规约就是(λ x y. x + y) a b → a + b。是不是很好理解。

通过这两个基本的公理,结合基本变量类型可以构造各种函数。如not函数,and函数,or函数,甚至if函数。

1
2
3
4
5
6
7
8
let and =
true value -> value
false value -> false
value true -> value
value false -> false

let if =
λ cond tvalue fvalue. (cond and tvalue) or (not cond and fvalue)

高阶函数

高阶函数意味着,我们可以把函数直接作为入参传入,或作为返回值返回。这早已不是函数式编程语言的专利,Python,JavaScript等也吸收了这个设计理念。

函数柯里化即部分求值,就利用了高阶函数的特点提出的技术,它使得函数可以一个一个接受入参,返回相同的计算结果。类似于下面的感觉:

1
2
3
4
5
6
function pow(i, j) {
return i^j;
}
funtion square(j) {
return pow(i, 2)
}

square函数返回的函数需要指定i才可执行。柯里的名字来自于第一次提出这个技巧的逻辑学家Haskell Curry

另外,值得注意的是,在函数式编程下,高阶函数通过将函数作为参数惰性求值实现。那命令式编程下呢,答案是闭包(lexical closure)。

递归?

函数式编程里没有状态变量(可以用其他方式实现),因此自然没有循环结构。实际上,函数式编程中的循环都是通过递归实现的。比如,斐波那契数列函数像下面这样:

1
let fact = λ n. if (n == 0) 1 (n * fact n-1)

这里fact函数引用了自身,虽然编译器可以识别这种写法,但是显然它并不符合严格的数学公理。

重新审视这个变换,我们可以通过传入自身的方式来让它“数学化”。let P = λ self n. if (n == 0) 1 (n * self(self n-1)),然后在令let fact n = P (P n)。如此这般:

1
2
3
4
5
6
7
fact 4
-> P (P 4)
-> if (4 == 0) (1) (4 * P(P 3))
-> 4 * P(P 3)
-> 4 * 3 * P(P 2)
-> 4 * 3 * 2 * P(P 1)
-> 4 * 3 * 2 * 1

可是,这个函数看上去并不自然,不像一个真正的递归函数,且λ演算的公理里并没有这样一条公理可以让你在定义函数的时候引用本身。还好,已经有人做了研究,借助Y组合子的帮助,可以实现真正的递归函数。

1
2
let Y = λ F. G(G)
G = λ self. F(self(self))

这相当于我们在λ演算公理体系中添加了一条“可以在函数调用时引用自身”。这也是证明λ演算图灵等价的关键一步。这意味着它的计算能力和计算机是一致的,能通过λ演算描述的函数一定可以由计算机计算。

Haskell

Haskell是一个纯函数式编程语言,它得名于上面提到过的Haskell Curry。Y组合子也是他发现的。

Haskell中一切都是函数,甚至没有指令式编程中变量的概念,它的变量全部都是只允许一次赋值,不可改变。

Haskell还没有一般意义上的控制流结构,如for循环,取而代之的是递归。同样,Haskell还有两个重要的特性,即无副作用和惰
性求值。偏数学的问题,用Haskell解决通常代码量都很小。下面是一个列表去重例子

1
2
3
4
5
6
cut cond  []  = []
cut cond (elem:rest) = if cond elem then
cut cond rest else elem:rest
compress [] = []
compress (elem:rest) = elem : compress
(cut (== elem) rest)

还有一个快排(不过借助了filter函数)的例子,也是短得不行

1
2
3
4
qsort (elem:rest) = (qsort lesser) ++ [elem] ++ (qsort greater)
where
lesser = filter (< elem) rest
greater = filter (>= elem) rest

Haskell中还可以定义无穷列表,如[1..]表示所有正整数。这也是惰性求值特性带来的。[1,3..] !! 42将会返回85。

Monad

Monad其实就是自函子范畴上的一个幺半群而已

这节将展示一个图文并茂的说明但并不致力于解释清楚monad到底是个什么(因为我自己也不明白)。这篇对比functor,applicatives,monad的文章写得很透彻易懂,尽管这可能并不能描述一个100%的monad。要更深刻了解monad还是需要学习范畴论的内容。

参考

函数式编程.pdf
Functional Programming For The Rest of Us
Functors, Applicatives, And Monads In Pictures

前言

Elm提出于2012年,最初出现在Evan Czaplicki的毕业论文中(掩面)。目标是提出一个面向web的函数式编程语言。它拥有诸多特性

  • 强类型语言
  • 一次编译,no runtime error
  • 状态不可修改
  • 函数是一级公民等

Elm是门新语言,它是一个类似React(但绝对和它不一样)的前端框架。在Web App的设计它甚至启发了Flux设计的提出。在如今前端框架吸收函数式编程以及强类型语言优点的形势下,学习Elm可能会开启对Web前端开发的重新认识。

再看完下面的介绍后,建议阅读两篇对Elm的评价,相信更有助于对这门年轻语言特点的理解:

准备工作

Elm是通过将代码编译成JavaScript工作的。一个最简单的Elm App大概像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)

main =
Html.beginnerProgram { model = 0, view = view, update = update }

type Msg = Increment | Decrement

update msg model =
case msg of
Increment ->
model + 1

Decrement ->
model - 1

view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (toString model) ]
, button [ onClick Increment ] [ text "+" ]
]

那么这样一个Elm文件是怎么应用在页面中呢?

Elm安装完成后,会有4个Elm相关的包:

  • elm-repl 命令行操作,在cli中感受Elm的语法特点
  • elm-reactor 快速脚手架搭建
  • elm-make 编译工具
  • elm-package 包管理工具

麻雀虽小,五脏俱全。

语法特点

Elm在语法上不同于C风格的所有语言,和Haskell更为靠近。

注释

--开启单行注释,结合{}进行多行注释

1
2
3
4
5
-- a single line comment

{- a multiline comment
{- can be nested -}
-}

类型

Elm是强类型语言,有Bool,Int,Float,Char,String 5种基本类型。有意思的是,Elm没有Null。这也是特别设计的。

注意:Elm中Char类型用单引号`'`包裹,而String类型用双引号`"`包裹。

除了基本类型,Elm中还有List,Array,Tuple,Dict,Record几种泛型。其中List,Record设计类似JavaScript中的Array和Object。

在Elm中`Array`和`List`是两种不同的类型,前者通过链表实现,后者通过Relaxed Radix Tree实现。前者在增删节点上高效,后者在随机查询时高效。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[1,2,3,4]
1 :: [2,3,4]
1 :: 2 :: 3 :: 4 :: []
point = -- create a record
{ x = 3, y = 4 }

point.x -- access field

List.map .x [point,{x=0,y=0}] -- field access function

{ point | x = 6 } -- update a field

{ point | -- update many fields
x = point.x + 1,
y = point.y + 1
}

*(值得注意的是,上面的::.x实际上都是函数)*。

类型声明

类型是Elm中重要的一环,每个变量都需要有类型,编译时需要进行检查。因此显式地声明变量类型很重要。

当我们需要通过基本类型构造复杂类型时,可以通过type alias的形式为record创建新的类型。在新类型创建的同时,会同步生成一个record构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
-- Before:麻烦的函数写法
hasBio : { name : String, bio : String, pic : String } -> Bool
hasBio user =
String.length user.bio > 0
-- After:简便的写法
type alias User =
{ name : String
, bio : String
, pic : String
}
hasBio : User -> Bool
hasBio user =
String.length user.bio > 0

Union类型

之所以单独拿出来说,是因为Union Type的设计几乎是Elm的精髓。首先,它类似于枚举(enum)的概念,通过type enum = A | B | C的形式定义一个类型。这是简单的Union Type的使用。

Union Type还有Tagged Union的用法。即下面这样。这意味着User可以是不带信息的Anonymous或带有String信息的NamedAnonymousNamed是User的两个构造函数。其中Named接受一个String类型入参构造User类型。

1
type User = Anonymous | Named String
注意:不同Union Type间的Tag构造函数不能同名,即不能出现下面的情况。同时Tag后的额外消息是泛型`a`时,需要在type名后也加上,便于type check
1
type User = Named | Named String -- Wrong!

结合上面的特点,可以很容易地将相似数据结构或设计抽象为单一模板,如举例中的将时间轴、日志等统一抽象为widget。思路:逐个击破,合而为一

由于Union Type可以递归定义,通过Union Type甚至可以构建链表和二叉树

1
2
type List a = Empty | Node a (List a)
type Tree a = Empty | Node a (Tree a) (Tree a)

此外,Elm中的错误处理MaybeResult也是基于Union Type实现的。

因为Union Type的不同子类型可以有自己独特的构造函数,且支持解构赋值,因此非常适合用作实现状态管理的事件。Web App中的update函数接受的Html Msg类型实际上也是Union Type实现的。

函数

除了不能手动更改状态的变量(因此递归完全替代了循环结构),函数是Elm中最常见的存在。命令式编程中,函数用作告诉电脑该怎么做。函数式编程中,函数用作描述一种映射关系,告诉电脑要什么。Elm中函数像下面这样声明:

1
2
3
4
5
6
7
8
9
square n =
n^2

hypotenuse : Float -> Float -> Float
hypotenuse a b =
sqrt (square a + square b)

distance (a,b) (x,y) =
hypotenuse (a-x) (b-y)

入参在上,返回值在下。入参间通过空格隔开,由于Elm支持函数柯里化,所以在函数的类型声明中,也是通过->隔开每个入参以及返回值的。由于函数变成了纯粹的“通道”,函数体中声明临时变量的语法通过let ... in的形式实现。

Elm中也有匿名函数,像下面这样,由反斜线\开头:

1
2
3
4
5
square =
\n -> n^2

squares =
List.map (\n -> n^2) (List.range 1 100)

函数间通过|><|连接减少括号的使用。例如下面这样

1
2
3
4
5
6
7
8
9
viewNames1 names =
String.join ", " (List.sort names)

viewNames2 names =
names
|> List.sort
|> String.join ", "

-- (arg |> func) is the same as (func arg)

同时,可以通过>><<构造复合函数。``用来将第n个函数入参前置,使得居于更符合语法习惯,如buyMilk `then` drinkMilkbuyMilk参数被提前。n可以通过infixr设置。

其中可能较难理解的是递归完全替代了循环结构,Elm类似其他函数式语言,没有forwhile。它用描述问题的方式,通过递归解决普通的循环问题,下面举两个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
reverse : List a -> List a
reverse list =
case list of
[] -> []
(x::xs) -> reverse xs ++ [x]

quicksort : List comparable -> List comparable
quicksort list =
case list of
[] -> []
(x::xs) ->
let
smallerSorted = quicksort (List.filter ((>) x) xs)
biggerSorted = quicksort (List.filter ((<=) x) xs)
in
smallerSorted ++ [x] ++ biggerSorted

可以找到规律:

Usually you define an edge case and then you define a function that does something between some element and the function applied to the rest.

更具体的解释可以看参考链接3。

控制结构

Elm中没有forwhile循环(都通过递归实现了)。但是存在ifcase语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
if powerLevel > 9000 then "OVER 9000!!!" else "meh"
case maybe of
Just xs -> xs
Nothing -> []

case xs of
hd::tl -> Just (hd,tl)
[] -> Nothing

case n of
0 -> 1
1 -> 1
_ -> fib (n-1) + fib (n-2) -- _表示不接受入参

杂项

  • ++连接字符串,+表示相加
  • Elm不会进行强制类型转换
  • 缩进会影响语句解析
  • list中所有元素类型必须一致
  • tuples设计类似python
  • records类型不允许访问不存在的属性
  • //用来进行C风格的除法
  • Elm中!用于连接Model和Cmd,用/=表示!=,同not表示!

Elm的语言设定大不同于C风格,所以,多写去熟悉它的语法风格吧。

Elm架构

Elm构建Web App的架构为MVU(不考虑Cmd和Subscription的话),这和目前大部分MVVM框架一样,致力于减少UI维护,减少不必要的状态修改来更好地定位错误。有意思的是,Elm也采用了Virtual DOM的设计

  • Model,类似Web App的state,通常为records类型
  • Update,更新state的唯一方式(类似于action或commit),通常类型为Msg -> Model -> (Model, Cmd Msg)
  • View,根据state渲染HTML的方式,通常类型为Model -> Html Msg

Update部分,通常像下面这样,接受Union Type的Msg,并通过case ... of接受到附在Msg上的payload。

1
2
3
4
5
update : Msg -> Model -> Model
update msg model =
case msg of
Change newContent ->
{ model | content = newContent }

View部分,所有常用的标签名和属性名都被封装为函数,接受属性列表和子元素列表两个入参,像下面这样。

1
2
3
4
5
6
view : Model -> Html Msg
view model =
div []
[ input [ placeholder "Text to reverse", onInput Change ] []
, div [] [ text (String.reverse model.content) ]
]

将M、V、U三部分结合在一起就可以构造简单的web应用了。当有异步任务等复杂情况出现时,需要通过Cmd和Subscription实现。

Effects

Elm Architecture

说这两位之前,我们要回顾下之前的工作流。仔细观察就能发现,我们所做的事只是描述了接受Html Msg后如何生产新的Model并根据新的Model生产新的Html Msg交给Elm Runtime,之后的脏活累活都交给Elm干了。根据Elm的描述,除了vDOM外,它还使用了下面两个手段提升效率:

  • Skip Work,通过lazy(类似React的shouldComponentUpdate)限制更新频率
  • Align Work,通过Html.keyed(类似React的key)减少无意义的diffing。

回到正题,Cmd和Subscription也是对Msg所做的外层包装而已。除了能向Elm Runtime pipe Html Msg外,当然也可以发送命令或订阅事件

。Html, Cmd, Sub三者实际上没太大区别,都是我们将生成的数据交给Elm Runtime,剩下的做甩手掌柜。它可以帮助我们:

  • 更好的debug
  • 确保Elm函数的线性时不变
  • 优化HTTP连接,缓存effects

下面是一个Sub的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
-- SUBSCRIPTIONS

subscriptions : Model -> Sub Msg
subscriptions model =
Time.every second Tick

-- UPDATE

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Tick newTime ->
(newTime, Cmd.none)

错误处理

之前提到过Elm致力于“编辑时无bug=>Runtime无bug”。除了强类型和无状态(实际上是Immutable的状态)外,还有Maybe和Result的辅助。

Elm treats errors as data.

参考null的糟糕设计,Elm通过

1
type Maybe a = Nothing | Just a

定义了Maybe类型。结合case of的特殊情况处理,通过option types的方法替代Nullcore/Maybe模块还有withDefaultandThen等其他方法。

类似于Maybe,Result用Union Type的形式定义了不可靠操作的返回值。

1
type Result error value = Err error | Ok value

通过Err和Ok两种类型,隐含了其他语言中的try catch操作,避免exception的出现。Elm库函数是实现了Result类型的,如Http.get。Result预定义在core/Result

Task

另外,Elm中有Task用来处理异步操作中的失败情况。类似于JS中的Promise。使用思路是先通过succeed方法和fail方法定义Task再使用。

1
2
3
4
type alias Task err ok =
Task err ok
succeed : a -> Task x a
fail : x -> Task x a

在Elm由0.17升级到0.18时,有两种方式执行一个Task。Task.performTask.attempt。前者针对从不返回错误的task,后者针对可能出错的task。更重要的是,在Task.attempt中结合了熟悉的Result模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
perform : (a -> msg) -> Task Never a -> Cmd msg
attempt : (Result x a -> msg) -> Task x a -> Cmd msg
-- example
type Msg = Click | NewTime Time

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Click ->
( model, Task.perform NewTime Time.now )

NewTime time ->
...

互操作性

Elm的互操作性体现在和JSON以及JavaScript上。

与JSON

通过Json.Decode和Json.Encode完成相关工作。解析部分由decodeString完成。

1
decodeString : Decoder a -> String -> Result String a

由第一个参数指定decoder类型,如 decodeString int "42"就指定了一个整数的解析器。在多数情况下,JSON字符串并没这么简单。这时,需要先利用基本的int、string、list、dict解析器结合map2、map3等构造相应的Decoder,再交给decodeString处理。函数返回Result类型

1
2
3
4
5
6
7
import Json.Decode exposing (..)

type alias Point = { x : Int, y : Int }

pointDecoder = map2 Point (field "x" int) (field "y" int)

decodeString pointDecoder """{ "x": 3, "y": 4 }""" -- Ok { x = 3, y = 4 } : Result String Point

是不是很麻烦。不像JavaScript里一个JSON.parse()完事,确实Elm解析JSON的笨拙为人诟病,官方的pipeline包通过|>组合Decoder,让语法稍微好了一点。最新的0.18版本下有json-extra提供更方便的decoder选择。

1
2
3
4
5
6
7
import Json.Decode.Pipeline exposing (decode, required)

pointDecoder : Decoder Point
pointDecoder =
decode Point
|> required "x" int
|> required "y" int

可这距离JSON.parse还是不够啊。于是有人写了个根据Records类型生成decoder的工具,或者在线生成

相比之下,encode过程就简单多了。

与JavaScript

有两种方式,port或flag。

前者类似于在Elm应用上凿洞,用类似订阅发布的模式工作。需要在文件开头的module声明前,额外加上port关键词,同时需要暴露的接口前也需要port关键词。

1
2
3
4
5
6
7
8
9
port module Spelling exposing (..)

...

-- port for sending strings out to JavaScript
port check : String -> Cmd msg

-- port for listening for suggestions from JavaScript
port suggestions : (List String -> msg) -> Sub msg
1
2
3
4
5
6
7
8
9
10
11
var app = Elm.Spelling.fullscreen();

app.ports.check.subscribe(function(word) {
var suggestions = spellCheck(word);
app.ports.suggestions.send(suggestions);
});

function spellCheck(word) {
// have a real implementation!
return [];
}

后者暴露program的init给外部JavaScript。在声明Elm program时,使用programWithFlags而非program

1
2
3
4
5
6
init : Flags -> ( Model, Cmd Msg )
init flags =
...

main =
programWithFlags { init = init, ... }
1
2
3
4
var app = Elm.MyApp.fullscreen({
user: 'Tom',
token: '12345'
});

Elm的思路是不向后兼容,避免过去问题的引入。只通过port和flags的方式,借由Elm runtime和外部JS沟通,避免自身的runtime exception,把问题只留在JavaScript部分。

工程实践

使用Elm编写简单的应用时,可能一个.elm文件完成后,就可以直接elm-make index.elm --output index.js就OK了。当工程较大时(目前还没怎么看到生产环境用Elm的),必然需要拆分组件。

Elm在设计上,拆分的比MVVM框架更细。它拆分到了函数的级别,由于它pure function的特点,不受状态的束缚,重用函数比MVVM框架顾虑少太多了。如,重用view函数就相当于React和Vue中的函数式组件。重用update函数,可以实现MVVM中“组件A改变组件B,组件B改变组件C,组件C改变组件A”的史诗级难题。

封装和应用是通过module ... exposing ...import ... exposing ...的语法完成的。Elm会去elm-package.json中的source-directories以及dependencies中声明的路径和包下寻找import对应的东西。剩下的,只要控制好复用程度,在习惯Elm语法后,就可以轻松且高逼格地编写Web应用了。

Elm编写好,且通过elm-make成功编译后,会得到一个.js文件,在需要的HTML文件中引入,会得到一个Elm全局对象,存储了所有的program。每个program都有embed方法和fullscreen方法绑定在HTML文件中。之后,大功告成!

参考作者所写的TODOMVC是个不错的开始。

FAQ

Q:Where are my components? And how do they communicate?
A:参见Scaling The Elm Architecture

Q:What’s the “Elm style”?
A:回头看看前言一节最后放的两个Elm开发感受,Elm中,View和Updates两部分均可以拆解成多个/组helper functions。最后在一个门面文件中汇总。Elm将state、updates、view放在一个文件,对开发更友好。Elm的优势在于函数式编程特点的帮助(无副作用/纯函数/强类型/出色的错误处理机制)和对人机交互模式的优异抽象。它的问题在

  • 不完善的文档
  • 蹩脚的互操作(interop)设计(port和flag)
  • 学习曲线陡峭
  • 缺乏成熟的成套解决方案
  • 坑爹的JSON支持

Q:Who will save my front-end routing?
A:有的,借助Elm Navigation package就可以做到,具体如何和状态变化绑定起来参见tutorial的routing部分

参考

下面的大部分问题来自Github的这个仓库,排名不分先后

46. html5中元素的data-*属性

用来存储用户自定义数据,可以通过Element.dataset.foo的形式读取。

47. CSS在前JS在后是一定的么

不。调试js,日志脚本,shim或polyfill以及动态修改整个页面style的JS代码需要放在<head>中。

48. progressive rendering

渐进式渲染技术是一种尽量快地将渲染结果展现给用户的技术。比如懒加载,优先渲染高优先级元素等手段。目标是将用户关心的内容优先展示出来,将页面信息一点点释放给用户。

49. normalize/reset CSS

知乎中一段说明写得太好,这里摘录下来。

CSS Reset 是革命党,CSS Reset 里最激进那一派提倡不管你小子有用没用,通通给我脱了那身衣服,凭什么你 body 出生就穿一圈 margin,凭什么你姓 h 的比别人吃得胖,凭什么你 ul 戴一胳膊珠子。于是 *{margin:0;} 等等运动,把人家全拍扁了。看似是众生平等了,实则是浪费了资源又占不到便宜,有求于人家的时候还得贱贱地给加回去,实在需要人家的默认样式了怎么办?人家锅都扔炉子里烧了,自己看着办吧。

Normalize.css 是改良派。他们提倡,各个元素都有其存在的道理,简单粗暴地一视同仁是不好的。body 那一圈确实挤压了页面的生存空间,那就改掉。士农工商,谁有谁的作用,给他们制定个规范,确保他们在任何浏览器里都干好自己的活儿。

Normalize.css是一种CSS reset的替代方案。Reset清除了所有浏览器默认样式,强调不同浏览器间的一致性。但是在我们需要浏览器默认样式时,就需要自己重头再来。Normalize是两位设计者研究不同浏览器默认样式差异后,设计的弥补浏览器bug保证一致性的CSS。现在大多数的网站已经抛弃了Reset.css而选择Normalize.css。

50. BFC是什么

BFC意为块格式化上下文(block formatting context),BFC表示一个区域,在区域内的所有子元素共同构成一个块级上下文。有4中手段可以触发BFC:

  • position: 不为static和relative
  • display: table-cell,table-caption,inline-block,flex,inline-flex
  • float: 不为none
  • overflow: 不为visible

相对来说通过overflow属性触发BFC对已有内容影响最小。利用BFC可以实现很多特性:

  • 消除box间margin的collapse现象
  • 包裹浮动元素,防止高度collapse
  • BFC不与其他元素相交叉,避免文字围绕图片,实现多栏布局

51. 清除浮动的一些手段

  • 在浮动元素后创建一个空的div标签,为之设置clear: both
  • 触发BFC,一般通过overflow属性触发
  • 使用伪类:after,设置clear: both

52. CSS sprites怎么实现和优缺点

将多张小图拼成大图后,通过background-imagebackground-position更改背景。优点是省流量,更换图标整体风格快;缺点是添加新图片麻烦。

53. 图片替代方案

图片替代指使用图片替代文字,同时保证图片可读,通常用于Logo。有下面几种方案:

  • display: none + background-image
  • text-ident: 9999px + background-image
  • margin-left: -9999px + background-image (盒模型很大,低效)
  • img alt属性
  • font-size或color: #fff

54. 解决browser-specific问题

写patches(独立的CSS文件)处理

55. 如何面对低等级浏览器

polyfill,graceful degration

56. 预处理语言的优缺点

优点:更加友善,更好的特性,语法糖
缺点:需要编译,应用场景局限

57. display有哪些可选值

  • none
  • inherit
  • initial
  • unset
  • block
  • inline
  • inline-block
  • flow
  • table
  • flex
  • grid
  • list-item
  • table-row-group
  • table-row
  • table-header-group
  • table-footer-group
  • table-cell
  • table-column-group
  • table-column
  • table-caption
  • inline-flex
  • inline-grid
  • inline-table
  • inline-list-item

58. translate和absolute的选择

两者并无优劣之分。只有应用场景的不同。translate可以实现复杂的位移和变形,absolute用在固定的定位时更方便

59. 视觉上隐藏一个元素

  1. position: fix + left: -9999px
  2. clip + width/height + overflow
  3. visibility: hidden

60. grid system

除开最新的Grid布局。可以通过inline-block + margin实现。grid布局目前支持还较差。这里是一篇很不错的介绍。

61. 高效的CSS写法

这里有一堆CSS编码风格。总体来说,要注意

  • 避免全局选择
  • 让选择器更加具体化
  • 减少不必要的选择器
  • 减少选择器的过深嵌套
  • 尽可能少使用表达式(即calc, rgba这些)
  • CSS放在头部

62. CSS匹配顺序

先构建DOM树,再从右至左地匹配CSS选择器

63. 盒模型

DOM元素以盒的形式呈现,包裹住真正的内容,有margin/border/padding/content四部分,width在默认情况下仅指content部分的宽度,height同理。若想改变盒模型,可以设置box-sizing属性

64. flex

flex是弹性布局。借助flex布局,可以很轻松地实现居中置右等使用默认方法难以实现的效果。flex布局有两个轴:主轴和交叉轴。元素在主轴方向上排布,在位置不够时,会沿交叉轴推挤到下一行或下一列。flex相关的CSS属性同时针对容器和项目。(下方加粗为默认值)

针对容器的属性有

  • flex-direction 主轴方向。可选row/column/reverse
  • flex-wrap 换行选项。可选nowrap/wrap/wrap-reverse
  • flex-flow 综合上面两个选项,如row wrap
  • justify-content 主轴上对齐方式。可选flex-start/flex-end/center/space-between/space-around
  • align-items交叉轴对齐方式。可选flex-start/flex-end/center/stretch/baseline
  • align-contents主轴间对齐方式。可选flex-start/end/center/space-between/space-around/stretch

针对项目的属性有

  • order 项目顺序,默认按照书写顺序排列
  • flex-grow 当容器主轴上长度足够时,该项目在主轴方向上的长度,默认为1,项目间按照该值比例分配
  • flex-shrink 当容器主轴上长度不够时,类似flex-grow处理
  • flex-basis 容器默认的主轴方向长度,也按比例分配
  • align-self 该项目的对齐方式

65. 适应式/响应式布局

responsive:响应式布局,使用同一种布局响应浏览器窗口的连续变化
adaptive:适应式布局,在视口特定大小时改变外观或样式,是离散的

66. DOM事件代理,冒泡和捕获两阶段

DOM事件代理是指,在DOM2级标准中,事件触发有捕获和冒泡两阶段,所以可以将事件监听器绑定在父节点上,减少EventListener的数目。细节可以参见之前做过的一则笔记

67. null, nudefined, undeclared三者的区别

null: 是Object类型,表示空对象,多用来表示还未赋值的对象
undefined: 是基础类型,表示没有定义的变量或属性
undecided: 只是一种称呼,表示没有用var, constlet声明的变量,默认为全局变量,应该避免出现这种情况

68. 匿名函数的应用场景

主要用作返回值或输入参数。

69. host object和native object的区别

前者是用户定义的对象类型,后者是环境自带的原生对象。尽量避免修改native object(包括增删改)。

70. Function.prototype.bind的使用场景

在setTimeout和listener handler中最常用到

71. feature detection/feature inference/UA

由于浏览器之间有各自的特性差异,这三种手段用于保证代码在浏览器间的兼容性。

  • feature detection 检测特性是否存在再使用,比较保险科学
  • feature inference 通过某特性是否存在推断另一特性是否存在,有风险,避免使用
  • UA 直接通过header中的User Agent来得到浏览器信息,建议迫不得已不使用

72. AJAX技术的优劣

优:用户体验好,局部刷新速度快,可以用于实现界面和数据分离
劣:相对来说较难调试,需要解决跨域问题,搜索引擎支持即SEO弱,会遇到移动端支持问题

73. JS templating

JS中的模板技术,如在backbone中使用的underscore的_.template方法。在xtemplate支持下,也可以在页面中指定<script type="x-template">的形式声明,

74. 如何理解不要改动built-in特性

浏览器和标准都是在不断变化的,此刻对built-in特性做的修改在之后浏览器或标注的呢更新后可能会埋下很深的坑。

75. 如何区分[]和{}

  • Object.Prototype.toString.call()
  • [].concat
  • instanceof
  • ES6中新增的方法Array.isArray

76. tenary operator

JS中唯一的三元操作符

77. DOM中attributes和properties的区别

节点的特性(attributes)用Attr类型表示。直观上讲,特性就是元素attributes属性中的节点,即在tag中声明的各特性名,以下面的标签为例:

1
<input type="text" value="John" />

该DOM节点有两个特性:typevalueAttr也是Node的一种,nodeType为2,Attr对象有三个属性

  • name 特性名称
  • value 特性的值
  • specified 特性是否指定在代码中,抑或是默认的

节点的属性(properties)则指对应的DOM对象的属性,不论是继承自Node或是Element类型的,还是自身类型自带的。比如上面同样的例子,该DOM节点具有children, childNodes, className等。

节点的属性和特性会有重合的部分,如id, type等,因DOM节点而异。上面的例子里,attribute中的value指声明在标签上的value默认值,而properties中的value则指该input标签当前的内容。

78. "use strict";

严格模式在ES5中引入,通过直接定义一个不赋给任何变量的上述字符串进入。可以选择在全局或是函数作用域内开启。

  • 严格模式下对默认模式下会静默出错的代码显式报错
  • 严格模式下禁止不合理的行为,如声明了两个一样的属性名
  • 严格模式还淘汰了一些属性,如arguments.calleearguments.caller; 同时,限制一些不安全的使用,如witheval
  • 严格模式抑制了this的值
  • 严格模式下,对未来版本可能用到的保留字禁止用户访问

不过,由于严格模式下代码的解析规则会不大一样,建议只在需要测试的特定函数内开启严格模式,

79. ready和load event的区别

  • ready在DOM元素加载完成后触发
  • load在页面所有资源请求完成后触发(包括图片、视频、音频等)

80. SPA的SEO优化

采用预渲染技术,或为爬虫专门准备静态页面

81. event loop,call stack和task queue

见这篇笔记

82. JavaScript中的对象和继承

见这篇笔记

不过,在ES6中引入了强类型OOP语言中传统的对象和继承语法。

  • 使用class关键字定义类,类中用constructor定义构造函数,使用publicprivate修饰成员级别
  • 可以在成员前指定getset为成员指定setter和getter函数
  • 通过extends实现继承

83. promise

见之前做过的一篇笔记

84. 提升有滚动条时的动画渲染性能

在CSS中为will-change属性指定动画要改变的CSS属性,参见MDN上的介绍

85. layout, painting, composition

浏览器解析,绘制,组合网页的过程。DOM操作可能会触发回流(reflow)或重绘(repainting),后者代价更小,建议减少频繁的DOM操作

86. 一些HTTP 1.1的header

  • Accept 接受文件的类型
  • Accept-Charset/Accept-Encoding 可以接受的文件字符集和编码
  • Age 从缓存实体产生到现在经历的时间
  • Allow 允许使用的HTTP方法
  • Cache-control 使用的缓存策略
  • Content-Encoding 响应体使用编码
  • Content-Lenght 响应体长度
  • Content-Range 响应体范围,用于部分下载(服务端的返回)
  • Content-Type 响应的媒体类型
  • Date 消息的发送时间
  • Etag html实体的唯一标识,用于缓存对比
  • Expires 缓存实体过期时间
  • Host 服务器的主机名,通常是请求资源的URL
  • Location 重定向的地址
  • Pragma 用于向后兼容还没有Cache-Control的HTTP1.0版本,通常只用作Pragma: no-cache
  • Range 请求资源的部分内容,一般用在多线程下载(客户端发起)
  • Referer 当前请求从哪个地址发起
  • Server 服务器端使用的软件信息
  • Transfer-Encoding 传输内容所用的协议类型
  • Upgrade 切换到额外的通信协议,服务端需要返回101状态码,并指定升级的协议名
  • User-Agent 请求发起自什么终端
  • Vary 列出一个相应字段列表,告知服务器当URL对应不同版本资源时,如何选择合适版本
  • Via 用在proxies中,表示使用的协议,版本以及起始端

更多header参考W3C文档

87. HTTP actions

  • OPTIONS 描述目标资源的通信选项
  • GET 获取数据
  • HEAD 类似GET,但是没有响应体
  • POST 将实体提交给服务器
  • PUT 用请求payload替换目标资源
  • PATCH 对资源部分修改
  • DELETE 删除指定资源
  • TRACE 沿着到目标资源的路径执行一个消息环回测试
  • CONNECT 建立一个到由目标资源标识的服务器的隧道

88. JS内存泄漏

  • 意外的全局变量
  • 被遗忘的setInterval
  • 脱离文档的DOM引用
  • 不合理的闭包

Chrome下可以通过Timeline/Profile选项卡查看内存使用情况,避免上述情况出现。

89. rem,em,px

Question 24

90. JS数据类型

基础类型:

  • Undefined
  • Null
  • Number(包括NaN Infinity)
  • Boolean
  • String

其余都是引用类型。更多参加这里

91. Object.assignObject.create

  • Object.assign 将传入变量的可枚举属性和已有属性合并
  • Object.create 创建一个以传入对象为__proto__的对象

92. 回流和重绘

还是参见how browsers work。为了减少回流,有下面一些推荐实践:

  • 一次性改变样式,如用class
  • requestAnimationFrame推迟回流
  • 虚拟DOM
  • 使用documentFragment

93. 排序算法

稳定:插冒归基;不稳定:快选堆希。实现略。

94. CSS/JS跨浏览器兼容问题

  • 明确产品的兼容版本方案,选择合适的技术栈
  • normalize.css polyfill/shim保证兼容
  • 在符合W3C标准浏览器下表现良好,旧浏览器下保证可用性,提示升级即可
  • CSS hack(特殊的选择器,条件样式表)

95. xss和csrf的防御

XSS(Cross Site Script,跨站脚本攻击),分为反射式,存储式,前者只对特定用户生效,如存储在个人资料中的脚本,后者对访问网站的所有用户生效,如攻击站点本身代码。防御转义时,不仅要监测<script>标签,对可以书写JavaScript的href属性,src属性,甚至imgonerror等事件也要做防御。

CSRF(Cross-site Request Forgery,跨站请求伪造),意为恶意网站通过用户存储的cookie,模拟向其他网站发起“合法”请求。需要注意下面两点

  • 不使用GET方法更新数据
  • 对于POST方法,通过后台生成随机的csrf_token注入到form<input type="hidden">标签中预防

总而言之,不信任用户的所有输入,对输入做处理,避免SQL注入和XSS攻击。


96. CSS属性继承

无继承性:

  • display
  • 文本属性:vertical-align/text-shadow/text-decoration
  • 盒模型属性
  • 背景相关属性
  • 定位属性:float clear top/left/right/bottom z-index
  • 轮廓内容:content outline

有继承性:

  • 字体属性
  • 文本属性:text-align/line-height/word-spacing/letter-spacing/color
  • 可见性:visibility
  • 表格列表:list-style
  • 光标:cursor

行内元素可继承:字体属性,除了text-indent/text-align
块元素可继承:text-align/text-ident

97. 移动端实现0.5px的border

  • 结合:before:after通过transform-originscale实现
  • 利用渐变background-image: linear-gradient
  • 直接使用backgroun-image

98. 随机打乱一个数组

Fisher-Yates shuffle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function shuffle(array) {
let counter = array.length;

// While there are elements in the array
while (counter > 0) {
// Pick a random index
let index = Math.floor(Math.random() * counter);

// Decrease counter by 1
counter--;

// And swap the last element with it
let temp = array[counter];
array[counter] = array[index];
array[index] = temp;
}

return array;
}

99. 移动端fixed定位bug

iOS中,在软键盘唤起后,fixed定位元素会失效,变为absolute定位。解决方案:主体部分设为height: 100%; overflow-y: scroll,通过absolute定位实现。

100. JS如何获知当前页面是否来自缓存

配合后台:后台传递时间戳到当前页面或cookie
无后台:通过xhr放送HEAD请求,得到返回的status code

101. 重复打印字符串

幂次叠加,substr切割字符串。

102. 正则匹配中?:?=的意思

  • ?: 非捕获匹配分组,匹配并出现在匹配结果中,但不作为子匹配返回
  • ?= 前瞻匹配,不出现在匹配结果中

更多参见问题16

下面的大部分问题来自Github的这个仓库,排名不分先后

18. Vue和React框架的对比

先说相同之处,Vue和React都是优秀的MVVM框架。具有响应式设计的种种特点,因此数据驱动是使用这两种框架不能忘的出发点。正如上一个问题提到的那样,开发者只需处理好数据,让框架去处理易错的UI。同时,组件化前端开发流程也是它们强调的一点,用组件化之名行代码重用之实,通过组件积木去构建整个页面。最后,它们不约而同地使用虚拟DOM树(vDOM)(Vue是在2.0引入的)抽象页面的节点,通过优化的diff算法减少频繁的DOM操作,减少交互的响应时间。另外,在最新的版本中,Vue和React的源码分别通过FlowTypeScript加入类型约束,增强可读性和健壮性。

两者同时也有MVVM框架共同的短板,把过多的渲染放在客户端,在页面元素复杂时,会加大交互的延迟。同时会影响首屏时间。

再说它们的不同之处,不同之处基本都在使用细节上。

  • Vue通常把网页模板写在template属性中,默认不支持JSX语法(可以通过插件支持)。React中则是写在render里
  • Vue糅合了Angular 1.x的特点,通过一些内置的自定义标签属性减少开发者的代码量。React则更加纯粹,少了这些预设的框架
  • Vue使用Object.defineProperty实现数据绑定,React通过setState显式更新依赖。前者更加酷和自然,但是在编程风格不好时,容易出现难以debug的错误。React推荐immutable Object,每次更新时使用新对象更新自身state,出错的概率更低。
  • Vue和React在组件生命周期上有些细微的差别。Vue的生命周期更加简明,在create,mount,update,destroy前后设置钩子函数,React在state改变前后也会有钩子函数
  • Vue的脚手架使用起来较React更加顺手(个人感觉),单文件.vue的组织方式,内部支持模板语言jade、ejs,预处理语言coffeescript,sass等。React的构建方式则更加自由。
  • React的社区较之Vue更为活跃些,流行的库要更多,脚手架中的库更多是爱好者自己开发的。Vue和React生态圈中都有路由和状态管理器的工具,其中Vue的都是官方开发的。

19. TCP三次握手,四次挥手过程

过程参考下图。

因为断开连接时数据可能并未传输完成,所以挥手时要多一步。

20. CSS预处理语言

Sass和Less。Sass支持变量的定义和使用,有语法控制结构,同时支持@mixin定义mixin和@function定义函数。更多介绍可以看我之前的一段笔记

less语法使用上和Sass相近,采用JavaScript实现。支持本地和在线解析。本人没有用过,更多特性参考官网

21. JS里的错误和异常处理

try catch语句块捕获错误,catch块接受一个参数作为错误对象。对象的message属性会给出错误的详细信息。catch后还可以接上finally关键字,finally语句块在错误处理后必定执行。throw语句可以抛出错误。

22. 闭包

闭包是JavaScript中比较有特色的概念。它和JS中的作用域链(见问题4)概念密切相关。闭包发生在函数中定义的函数,在外层函数退出后,其作用域环境通过作用域链仍然保存在存活的内部环境中。利用这种特点,可以实现诸如状态保存,封装等特殊性质。

值得注意的是,内层函数的不合理操作会导致内存泄漏。且大量使用闭包会导致性能问题。不要过度依赖闭包。

23. 列举一些RESTful操作名

GET/POST/DELETE/UPDATE/PUT等

24. 列举一些CSS中的长度单位

固定长度:

  • px 像素点
  • mm 毫米
  • cm 厘米
  • in 英寸
  • pt 磅(1/72英寸)
  • pc 活字(1/6英寸)

相对长度:

  • em 以字体大小(font-size)为单位
  • ex 以小写字母大小为单位
  • rem 以根元素(默认为<html>)的字体大小为单位,用于自适应布局
  • vh 视口高度的1/100
  • vw 视口宽度的1/100
  • vmin 视口宽高较小值的1/100
  • vmax 视口宽高较大值的1/100

25. 前端职责

狭义地来说是实现UI设计师的设计稿和UE、UX的交互细节。宏观来说,是借助浏览器在技术上处理和用户交互的所有环节。近些年来,借助native的帮助,前端还可以实现后台的业务部分。

26. 职业规划

27. 获取新鲜资讯的方式

28. 前端模块化

模块化规范包括CommonJS,CMD(SeaJS),AMD(RequireJS)等。我此前做过一个小型的笔记

29. position类型

static, relative, absolute, fix四种。

  • static 正常文档流
  • relative 正常文档流,指定top, left等CSS属性相对原位置移动
  • absolute 脱离文档流,相对上一个非static元素定位
  • fix 脱离文档流,相对html定位

通常建议使用正常文档流减少潜在bug。绘制复杂动画时,建议使用脱离文档流的布局。

30. 盒模型

盒模型意为所有渲染的元素都是一个个的矩形。矩形区域内包含margin, border, paddingcontent4层。CSS属性中只有两种盒模型:border-boxcontent-box。后者是默认值。可以通过box-sizing属性设置。

31. 使用原生JS发送AJAX

现代浏览器中都通过XMLHttpRquest对象实现Ajax请求。古老的IE浏览器会有不同的实现方法(目前已经很少见了),如:

1
2
new ActiveXObject("Microsoft.XMLHTTP");
new ActiveXObject("Msxml2.XMLHTTP.6.0");

通常使用XMLHttpRequest发送GET的方法如下:

1
2
3
var xhr = new XMLHttpRequest();
xhr.open("GET", "demo.php?id=1");
xhr.send()

发送POST和其他复杂请求时,需要设置request header,在需要接受AJAX返回时,还可以监听readyState的change事件
,大致像下面这样:

1
2
3
4
5
6
7
8
var xhr = new XMLHttpRequest();
xhr.open("POST", "ajax_post.php");
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log("ok" + xhr.responseText);
}
}

XMLHttpRequest2中又引入了进度,跨域,中止等新特性。

另外,使用新的Fetch API也可以完成Ajax请求。Fetch提出的目标是提供访问和操纵HTTP的接口,异步的获取网络资源。它和XMLHttpRequst的区别是:

  • 即使响应是404或500,返回的Promise也会正常解决
  • 默认情况下,fetch在服务端不会发送或接受任何cookies

在Ajax需要跨域时,最常用的方法是使用jsonp的形式实现。不过目前通过XMLHttpRequest2或fetch也都能完成跨域请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function jsonp(url, success) {
var ud = '_' + +new Date,
script = document.createElement('script'),
head = document.getElementsByTagName('head')[0]
|| document.documentElement;
window[ud] = function(data) {
head.removeChild(script);
success && success(data);
};
script.src = url.replace('callback=?', 'callback=' + ud);
head.appendChild(script);
}
jsonp('http://soundcloud.com/oembed?url=http%3A//soundcloud.com/forss/flickermood&format=js&callback=?', function(data){
console.log(data);
})

31. 如何加快访问速度

这个问题实际上很大,可以从各个方面去优化

  • 压缩并打包资源文件
  • 使用CDN存储资源文件
  • 设置缓存
  • CSS sprites
  • 图片压缩
  • 图片懒加载
  • 合理的DOM层级设置
  • <script>标签设置deferasync或动态异步加载
  • 一般情况下,CSS在前,JS在后
  • 使用MVVM框架时,使用服务端渲染或预渲染

32. 前端的未来发展

随着大前端的风吹来,前端工作在横向和纵向都获得了更多的机会。横向上,由PC端到移动端甚至有界面展示的智能终端。不过后两者需要记住native库的帮助。纵向上,在NodeJS的帮助下,后台的部分业务功能抽离出来交由前端完成,前端对界面有完整的控制,数据通过接口的形式向后台索取。现在看来,身为一个前端工程师,不仅要对HTML,CSS,JavaScript老三样了如指掌,对Android或iOS也渐渐有了些要求。

33. CSS selector的优先级顺序

how browsers work中有介绍。根据CSS3 selectors specificity中的定义,一个选择器的优先级计算如下

  1. 如果声明来自于style属性,而不是带有选择器的规则,则记为1,否则记为0 (= a)
  2. 记为选择器中ID属性的个数 (= b)
    3.记为选择器中其他属性和伪类的个数 (= c)
  3. 记为选择器中元素名称和伪元素的个数 (= d)

将四个数字按a-b-c-d这样连接起来(位于大数进制的数字系统中),构成特异性。所使用的进制取决于上述类别中的最高计数。最终决定优先级顺序。简而言之就是,style > id > class > tag > pseudo class,统计情况下看个数,有!important时,以!important为准。

34. 连等的赋值顺序

问题是,有这样一段代码,问原理是什么:

1
2
3
4
var foo = { n: 1 };
var bar = foo;
foo.x = foo = { n: 2 };
console.log(foo.x); // undefined

我们来细化一下过程,首先我们要明确JavaScript中对象是引用类型:

  • 第一句里,首先创建了一个字面量对象,并将foo指向之
  • 第二句里,将foo赋值给bar,即bar也指向{ n: 1 }这个对象
  • 第三句里,又创建了一个{ n: 2 }的对象,首先寻找foo指向对象中是否有x属性,没有时则新建一个x属性指向这个字面量对象,接着改变foo存储的地址,指向这个新的对象。
  • 第四句里,由于新的对象没有x属性,foo.x将返回undefined

这时,如果我们console.log(bar)结果将是{x: {n: 2}, n: 1}`。

35. JavaScript的hoist行为

hoist特性又叫变量声明提升。是JavaScript中比较有特点的特性。意为将作用域中所有变量(包括函数)声明提升到语句的开头。如下面的语句

1
2
3
4
5
6
7
a = 1;
fun(a);

var a = 0;
function fun(num) {
return num + 1;
}

等同于

1
2
3
4
5
6
7
8
var a;
function fun(num) {
return num + 1;
}
a = 1;
fun(a);

a = 0;

需要注意的是当通过函数表达式声明函数时,会提示fun未定义。因为,此时的语句等同于

1
2
3
4
5
6
7
8
9
var a;
var fun;
a = 1;
fun(a);

a = 0;
fun = function(num) {
return num + 1;
}

最后,最佳的风格是所有的变量先声明再使用。这也是JSLint和JSHint等linter工具推荐的。

36. 优雅降级和渐进增强

又名graceful degration和progressive enhancement。是两种开发的思路。前者意为针对最高级的浏览器设计开发,再保证向下兼容;后者意为针对低版本浏览器设计,保证基础性能,再追对现代浏览器追加效果,提高用户体验。

37. 优化网页资源

  1. 使用CDN
  2. 分布存放
  3. CSS sprites
  4. disable etag

38. 浏览器一次可以从同一domain加载多少资源

2~8,因浏览器而异。这里有个非常全的表。

39. 轮播图设计思路

可以用display实现,配合渐变效果。需要看到幻灯片移动时,可以结合transform和scale实现,配合overflow: hidden

40. CSS3的部分新特性

  • border: border-radiusborder-image
  • background: background-sizebackground-origin
  • text: font-facetext-overflowtext-shadowword-breakword-wrap
  • transform: translaterotateskewscale
  • transition: transitionanimation
  • other: box-shadow

41. ARIA

ARIA全称Accessible Rich Internet Application。主旨是提升网页易用性,方便有阅读障碍的人使用。比较常用的属性有

  • role 当前元素的作用,
  • <label>, aria-label 元素名
  • aria-hidden 是否隐藏

42. CSS animation和JavaScript animation

前者方便简单,通过keyframe就可以画出动画,且浏览器会做一些优化,因此性能也比较好。后者更加灵活,可以暂停和反转,且支持交互性。更详细的分析参考这里

43. doctype是什么

doctype出现在HTML4.1。用于规范HTML和XML文档格式,在推出时,HTML有3种标准可以选择:strict, transitional, frameset。

1
2
3
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">

在HTML5中,只有一种doctype可以选择,那就是html:

1
<!DOCTYPE html>

44. standard mode和quirks mode

分别是标准模式和怪异模式。由于历史原因,为了兼容标准建立前就已存在的古老网站,浏览器存在着两种解析网页的模式。在怪异模式下,排版会模拟Navigator 4与Internet Explorer 5的非标准行为。为了支持在网络标准被广泛采用前,就已经建好的网站,这么做是必要的。在标准模式下,行为即(但愿如此)由HTML与CSS的规范描述的行为。在<!DOCTYPE>中指定html将自动启用标准模式。这里有更多介绍。

45. XHTML和HTML

XHTML伴随HTML4.01一同提出,使用更加严谨的语法。MIMEtype为application/xhtml+xml,比如:html元素需要有xml相关属性,元素名必须是小写字母,元素属性用"包围不能为空值,在内容里不能有&, 需要转义,包括其他特殊字符<>,空元素以/>结尾。由于语法过于严苛,使用的人不多。未推出的XHTML1.1便被html5取代。

下面的大部分问题来自Github的这个仓库,排名不分先后

1. JS中如何定义自定义事件,实现订阅/发布模式

明确需求:可以通过onemit绑定和触发事件。

方案:创建全局事件管理器events,构建事件名和回调函数数组的键值对。onemit分别写和读events。大概像下面这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var EventUtil = {
// 全局事件管理
var events = {},
// 注册事件
on = function (type, handler) {
if (events[type]) {
events[type].push(handler);
} else {
events[type] = [handler];
}
},
// 触发事件
emit = function (type) {
if (!events[type]) return;
for (var i = 0, len = events[type].length; i < len; i++) {
events[type][i];
}
};
};

当使用Object.assign实现继承时,会出现events共享的问题。可以通过在第一次调用on时,通过Object.defineProperty的方式创建避免共享。

2. js中的this

首先,this永远是对象。

  • 全局上下文内,this全局对象
  • 函数上下文内,根据调用场景分情况讨论
    • 直接调用:全局对象
    • 通过对象的方法调用:调用方法的对象
    • 构造函数中:即将被创建的对象,有return语句时,以return的返回值为准
    • call和apply:传入的第一个值
    • bind方法:永久绑定到第一个参数

3. js跨域问题和解决方案

跨域(Cross-domain)是网景最初基于安全性考虑提出的策略。意为不同域名不同协议不同端口间的Ajax通信是被禁止的。根据使用需求,可以分为跨站请求资源和跨页面共享资源(我自己发明的说法)

跨站请求资源

  • jsonp(json with padding)跨域,利用了<script>标签的可跨域完成,自己写一遍就能搞懂
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function getJSONP(url, success) {

    var ud = '_' + +new Date,
    script = document.createElement('script'),
    head = document.getElementsByTagName('head')[0]
    || document.documentElement;

    window[ud] = function(data) {
    head.removeChild(script);
    success && success(data);
    };

    script.src = url.replace('callback=?', 'callback=' + ud);
    head.appendChild(script);
    }
  • CORS,使用CORS进行跨域请求时,在现代浏览器中已经可以像普通Ajax请求那样使用XMLHttpRequest即可,可以参考这个。需要后台服务器支持
  • 后台反向代理,需要一台中转的服务器
  • 建立websocket通信,需要后台服务器支持

跨页面共享资源,结合<iframe>有以下几种方案

  • 修改document.domain,使两个页面位于同一域名下,注意只能从精确->模糊修改域名
  • 通过window.name传递消息,利用了iframe location变化后,window.name不变的特点
  • location.hash
  • html5中的postMessage API在不同window间传递消息

这里附上一个链接.

4. js的作用域链

这是JavaScript最有特点同时也是最基础的内涵之一。红宝书和犀牛书都做了详尽和透彻的解释。这个问题理解了,什么是闭包就能很好地理解了。

执行环境是JavaScript中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。虽然我们便习得代码无法访问这个对象,但解析器在处理数据时会在后台使用它。

全局执行环境是最外围的一个执行环境。…。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局环境知道应用程序退出时才会销毁)
每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行后,栈将其环境弹出,把控制权返回给之前的执行环境。ECMAScript程序中的执行流正是有这个方便的机制控制着。

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,时钟都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象做为变量对象。活动对象在最开始时只包含一个变量,即arguments对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而在下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中最后一个对象。

5. Function.callFunction.apply的区别

callapply同为改变this的方法,前者一个一个接收输入参数,后者以数组的形式接收。

6. 浏览器渲染页面的原理

可以参考经典的文章how browsers work,或者中译版.

7. 列举一些HTML5的改进

可以参考MDN给出的summary。比如:

  • 语义化,语义化标签<header>, <article>,语义化元素<figure>, <time>等和新的多媒体标签<audio>, <video>
  • 网络通信,Websocket,WebRTC
  • 图像,Canvas和WebGL
  • 离线存储,Storage接口和IndexDB
  • 性能,Web Worker,XMLHttpRequest2(支持进度等新特性),History API,Fullscreen,PointerLock,requestAnimationFrame等
  • CSS,CSS3的特性,有些甚至演进到了Level 4

8. HTML5中的定位API

Geolocation API,新的API,红宝书中有提到。通过navigator.geolocation对象实现,允许用户提供自己的所在地理位置。需要用户确认同意才可使用。最常用的方法是getCurrentPosition()。这个方法接受三个参数——成功回调、可选的失败回调、可选的选项。

类似的不常见的API还有Battery APIFile APIperformance等。

9. 一些前端框架的双向绑定原理

不是所有框架都提倡双向绑定。有的框架如Angular使用数据双向绑定,适合于表单很多的站点,React和Vue这样的使用的是单向绑定。在单向绑定背景下,可以通过addEventListener实现双向绑定。

实现原理上分为几种:

  • 发布-订阅模式,显式地绑定事件和监听函数。backbone就是这么做的,显式地通过Model.set修改数据触发change事件来更新视图。React也是通过setState显式地触发虚拟DOM树更新和重新渲染的。
  • 脏检查(digest cycle),通过特定事件触发脏检查。脏检查即一种不关心你如何以及何时改变的数据,只关心在特定的检查阶段数据是否改变的数据监听技术。过程大致是$update或其他手段触发digest阶段,遍历通过$watch绑定的watcher。对比值是否改变触发更新。优点是无需知道更改数据的方式,可以统一更新view,缺点是watcher较多时会有严重的性能问题。
  • 数据劫持Object.defineProperty,Vue使用这种方式实现隐式的绑定(当然在具体实现中复杂了许多)。这么做的问题是版本只支持到IE9+,且在数组更新时有所局限。

10. webpack的配置文件写法

除了常用的entry, output, module, plugins外,webpack的使用方法实在太多,建议去官网查看完整的配置信息。

11. node文件和网络API

文件操作上,常用的有fs.readFileSyncfs.writeFileSync,或通过流的方式使用fs.createReadStreamfs.createWriteStream。还有pipe将流连接在一起。除此之外,pathjoinnormalize常用在处理文件路径。

和网络操作相关的包包括http, https, url, querystring, zlib等。其中前两个包更为常用,尤其是http.createServer方法。

另外,在进程上有process, child_process等包。这里 有一篇文章做了比较详细的介绍。当然,有空最好还是去官方文档.

12. 和@import的区别

它们的最常见的使用方式都是引入CSS文件到html中。它们的区别在于

  • link是XHTML标签,除了加载CSS外,还可以引入RSS等其他资源;@import属于CSS范畴,只能加载CSS。
  • link引用CSS时,在页面载入时同时加载;@import需要页面网页完全载入以后加载。
  • link是XHTML标签,无兼容问题;@import是在CSS2.1提出的,低版本的浏览器不支持。
  • 由于link是标签,可以通过JavaScript控制来改变样式,后者不行。

13. cookie,localStorage和sessionStorage的区别和联系

cookie设计的初衷是用来为无状态的http访问添加用户状态消息的。大小4KB以下,会携带在请求头中。大多包含敏感信息,会和服务器端的session配合使用。

Storage API是HTML5的新API。又可以细分为localStorage和sessionStorage。它们一般只存储在客户端,用来缓存用户非敏感数据,大小因浏览器而异,容量约可达到5MB。sessionStorage在浏览器关闭后清除,localStorage则在超过时限或手动clear后清除。

cookie中的内容很少变化,且最好秘文储存,并通过HttpOnly添加限制(后台修改set-cookie头)。Storage则很可能会频繁读写。

14. HTTP状态码

根据状态码开头的数字确定状态码类型。下面列举一些常用的。

1xx 信息:这一类型的状态码,代表请求已被接受,需要继续处理。这类响应是临时响应。

  • 100 继续:客户端应当继续发送请求。
  • 101 切换协议:将通过Upgrade消息头通知客户端采用不同的协议来完成这个请求。

2xx 成功:这一类型的状态码,代表请求已成功被服务器接收、理解、并接受。

  • 200 OK:请求已成功,请求所希望的响应头或数据体将随此响应返回
  • 201 已创建:请求已经被实现,而且有一个新的资源已经依据请求的需要而创建,且其URI已经随Location头信息返回
  • 202 已接受:服务器已接受请求,但尚未处理
  • 204 No Content:服务器成功处理了请求,但不需要返回任何实体内容,用户浏览器应保留发送了该请求的页面
  • 205 Reset Content:和204的唯一不同是返回此状态码的响应要求请求者重置文档视图
  • 206 服务器已经成功处理了部分GET请求。该请求必须包含Range头信息来指示客户端希望得到的内容范围,多用于下载工具

3xx 重定向:这类状态码代表需要客户端采取进一步的操作才能完成请求。通常,这些状态码用来重定向,后续的请求地址(重定向目标)在本次响应的Location域中指明。

  • 300 多选择:被请求的资源有一系列可供选择的回馈信息,每个都有自己特定的地址和浏览器驱动的商议信息。用户或浏览器能够自行选择一个首选的地址进行重定向。
  • 301 永久移动:被请求的资源已永久移动到新位置
  • 302 临时移动:请求的资源现在临时从不同的URI响应请求
  • 303 重定向:对应当前请求的响应可以在另一个URI上被找到,而且客户端应当采用GET的方式访问那个资源
  • 304 如果客户端发送了一个带条件的GET请求且该请求已被允许,而文档的内容(自上次访问以来或者根据请求的条件)并没有改变
  • 305 使用中介:被请求的资源必须通过指定的代理才能被访问

4xx 客户端错误:代表了客户端看起来可能发生了错误,妨碍了服务器的处理

  • 400 无法理解的请求:由于包含语法错误,当前请求无法被服务器理解
  • 401 需要验证:当前请求需要用户验证。响应必须包含一个适用于被请求资源的WWW-Authenticate信息头用以询问用户信息。
  • 403 禁止访问:服务器已经理解请求,但是拒绝执行它
  • 404 未找到:请求所希望得到的资源未被在服务器上发现
  • 405 方法不允许:请求行中指定的请求方法不能被用于请求相应的资源,响应中必须返回一个Allow头信息用以表示出当前资源能够接受的请求方法的列表
  • 406 头部不对:请求的资源的内容特性无法满足请求头中的条件
  • 408 请求超时:客户端没有在服务器预备等待的时间内完成一个请求的发送
  • 411 需要指定长度:服务器拒绝在没有定义Content-Length头的情况下接受请求
  • 413 请求实体太长
  • 414 URI太长

5xx 服务器错误:代表了服务器在处理请求的过程中有错误或者异常状态发生

  • 500 内部错误:一般来说,这个问题会在服务器的代码出错时出现
  • 501 未实现:服务器不支持当前请求所需要的某个功能
  • 502 Bad GateWay:作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应
  • 503 服务不可达:由于临时的服务器维护或者过载,服务器当前无法处理请求。这个状况是临时的,并且将在一段时间以后恢复。
  • 504 网关超时:作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器或者辅助服务器收到响应
  • 505 HTTP协议版本不正确

15. URL去参数

location.search, location.href, location.origin分别代表url中的querystring,完整url和域名。再结合location.pathname, location.portlocation.protocol可以得到任意想要的URL参数。

另外,新的APIURLSearchParams中有些方法可以对querystring做方便的增删改查的操作。

  • append增加一个检索参数
  • delete删除一个检索参数
  • get获取检索参数的第一个值
  • getAll获取检索参数的所有值
  • has检查是否存在某检索参数
  • set设置一个检索参数的新值,会覆盖原值
  • keysvalues分别返回键和值组成的数组

16. js中的正则匹配

js中的正则匹配和Perl的正则匹配规则基本类似。在js中,使用一个正则表达式字面量,由包含在斜杠之间的模式组成。正则表达式同时也是RegExp对象。除了简单模式外,考察对正则表达式的熟悉在它的特殊字符使用上。

一些常见的特殊字符:

  • \ 用于转义

  • ^ 用于匹配开始或表示一个反向字符集(如[^xyz]

  • $ 用于匹配结尾

  • * 匹配前一个表达式0或多次 = {0,}

  • + 匹配前一个表达式1或多次 = {1,}

  • ? 匹配0或1次 = {0,1};紧跟量词后使匹配非贪婪

  • . 匹配除换行符外任何单字符

  • (x) 捕获匹配,会包括在最后结果中,也可以通过$1, $n来访问

  • (?:x) 非捕获分组,匹配但不捕获

  • x(?=y) 断言匹配,捕获后跟y的x

  • x|y 匹配x或y

  • {n} 量词,匹配n次,还有{n,m}和{n,}的用法

  • [xyz] 字符集,可以使用-连接,如[x-z]

  • \d 一个数字

  • \D 一个非数字

  • \s 一个空白字符,包含空格,制表符,分页符,换行符

  • \S 一个非空白字符

  • \w 一个单字字符,等价于[A-Za-z0-9_]

  • \W 一个非单字字符

另外,正则表达式还有几个可选参数辅助搜索类型

  • g 全局搜索
  • i 不区分大小写
  • m 多行搜索
  • y 粘性搜索

有一些方法用于和正则表达式相关

  • exec 在字符串中执行匹配,返回匹配结果
  • test 测试是否能匹配RegExp,返回true或false
  • match 对字符串执行查找匹配的String方法,返回匹配结果
  • search 在字符串中测试匹配,返回位置索引或-1
  • replace 在字符串中执行查找匹配,并使用替换字符串替换匹配的子字符串
  • split 使用一个正则表达式或字符串分割一个字符串,并储存在数组中

常见的考法有,书写一个邮箱或手机号的正则表达式:

  • 邮箱 /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
  • 手机号 /^0*(13|15|18|14|17)(\d{9}|\d-\d{3}-\d{5}|\d-\d{4}-\d{4}|\d{2}-\d{3}-\d{4}|\d{2}-\d{4}-\d{3})$/

17. MVC和MVVM框架

框架模式不是一门写代码的学问,而是一门管理与组织代码的学问。其本质是一种软件开发的模型。与设计模式不同,设计模式是在解决某类特定问题时总结抽象出的公共方法,是方法论的范畴,一种框架模式往往使用了多种设计模式,且和技术栈有耦合的关系。

视图(View)从本质上讲是数据在图像上的一种体现和映射。用户在操作图像时可以达到操作数据的目的,在数据更改后,需要重新将数据映射到视图上。这实际上就是MVC的出发点。

  • View: 放置视图相关的代码,原则上里面不应该有任何业务逻辑。
  • Controller: 放置视图与模型之间的映射,原则上这里应该很薄,他只放一些事件绑定相关的代码(router),但并不实现真正的功能,他只是一个桥梁。
  • Model: 这里的model不是说实体类,它是主要实现业务逻辑的地方。

开发流程是先创建视图组件,再将之关联到Model上,通过View修改Model中的值时,Model会触发绑定在之上的所有View的更新。Backbone是个典型的例子。这么做部分分离了视图和逻辑。但是,在情况复杂时,Model的代码量将会大大膨胀。

MVP因此而生,其中Presenter(分发器)代替了原来的Controller,分担了Model的部分功能。针对上面的问题,Presetner隔断了Model和View,当M改变时,会通知P去更新视图。业务逻辑和绑定逻辑从V和M中分离出来到P中。使得MVP三方分工更加鲜明。绝大多数的PHP框架都是MVP类型的。

MVVM是Model-View-ViewModel的缩写。在MVVM中,View和ViewModel是双向或单向数据绑定的关系。当ViewModel反应了Model中的数据模型,并绑定到视图属性上,反过来,视图属性变化后也会通过ViewModel影响Model。React,Vue这些流行的前端框架都是MVVM类型的。

不管是MVC还是MVP或MVVM,他们都是数据驱动的。核心上基于M推送消息,V或P来订阅这个模型。使用者需要维护的不再是UI树,而是抽象的数据。当UI的状态一旦多起来,这种框架模式的优势就很明显了。

0%