前端工程师首先是软件工程师

作为一名工程师, 你的工程管理, 技术方案设计, 架构规划等等能力才是核心, 是不会随着某个领域消失而消失的, 这些能力是和整个大的软件工程体系绑定的, 拥有和算法, 计算机底层原理一样的生命力, 值得你不断的去学习和提升, 我相信只干前端, 你难过 35, 但是作为工程师, 你应该拥有更长的职业寿命

其实这是一篇今天的学习笔记。

吐槽下近况

五月底从上家公司离职回到了成都,到这个端午假期一个月时间,断断续续面试了几家公司,最终的结果是:拿到Offer的不想去,想去的又不给offer。。。

受疫情的影响(不知道是不是真的受疫情的影响),今年招聘的公司确实不太多,招聘的公司要求感觉也都提高了。

离职前自己也有准备,也有刷题,按照以前的面试情况,这次准备在知识广度上多花了些时间,深度上花的时间不多。可谁知道,最近面试的有意愿想去的公司,不仅知识面问得广,各种原理也问的多,就算答出了通过刷面经能获得一些比较表面的原理,面试官还会继续深挖一下,直到你说出:“这一点我还不太熟悉”为止。

收到一封面试后的拒信,感受到对小团队、自己想怎么玩怎么玩、又不是不能用、单兵作战等开发方式的强烈鄙视?。?
bye.png

在一个位置待久了,如果只是长期做一个按时完成领导任务的复制-粘贴工程师,没有自己花时间去深入学习的话,面试可能会碰的鼻青脸肿。

最近多次看到这样的一段文字: 前端工程师首先是软件工程师 。以前没太注意,现在想想,如果只把自己定义成一个前端工程师,那么将来的路真的只会越走越窄。 所以我们不仅要掌握多重专业技能之余,也应具备最基本的软件理论知识。

前几天看到一篇文章:前端职业规划 - 非大厂前端工程师如何不断提升自己的技术和专业能力,其中有这样一段描述:「作为一名工程师, 你的工程管理, 技术方案设计, 架构规划等等能力才是核心, 是不会随着某个领域消失而消失的, 这些能力是和整个大的软件工程体系绑定的, 拥有和算法, 计算机底层原理一样的生命力, 值得你不断的去学习和提升, 我相信只干前端, 你难过 35, 但是作为工程师, 你应该拥有更长的职业寿命」,感觉每一个 Coder 都值得去思考。

离职后有开始接触一些以前觉得对业务工程师收益并不明显的一些东西,现在想想,也许收益并不是立杆见影,但是长远来看,还是有益于自己职业生涯。

开始正文:

虽然前端领域瞬息万变,但也有一些具备 「一次学习,终生受用」 特性的知识,比如:

  • 设计模式
  • 性能优化核心思路(主要关注渲染性能网络性能

基础理论知识是一个人的基线,理论越强基线越高。再为自己定一个目标和向上攀附的阶梯,那么达到目标就是时间问题,而很多野路子工程师搞了半辈子也未达到优秀工程师的基线,很多他们绞尽脑汁得出的高深学问,不过是正规工程师看起来很自然的东西。—— 吴军

本文「设计模式」部分是阅读了大佬的掘金小册:《JavaScript 设计模式核⼼原理与应⽤实践》的学习笔记。

本文「算法」部分是阅读了大佬的掘金小册:《前端算法与数据结构面试:底层逻辑解读与大厂真题训练》的学习笔记。

设计模式

对被用来在特定场景下解决一般设计问题的类和相互通信的对象的描述。——《设计模式:可复用面向对象软件的基础》

设计模式核心思想:封装变化

在实际开发中,不发生变化的代码可以说是不存在的。我们能做的只有将这个变化造成的影响最小化 —— 将变与不变分离,确保变化的部分灵活、不变的部分稳定。这样才能写出「健壮」的代码。

设计模式原则:SOLID原则

  • 单一功能原则(Single Responsibility Principle)
  • 开放封闭原则(Opened Closed Principle)
  • 里式替换原则(Liskov Substitution Principle)
  • 接口隔离原则(Interface Segregation Principle)
  • 依赖反转原则(Dependency Inversion Principle)

在js中,主要围绕「单一原则」和「开放封闭原则」展开,前端中主要掌握这两就好了。

设计模式大概被分为 23 种,根据每种模式用来完成什么工作,将23种分成了三大类:

  • 创建型:与对象的创建有关
  • 结构型:处理类或对象的组合
  • 行为型:对类或对象怎样交互和怎样分配职责进行描述。

我们需要掌握其中一部分,了解剩下的部分。这里只列出我们应该掌握的部分,其他的请自行了解~

1、构造器模式

这应该是我们最熟悉,最常见的设计模式(你肯定用过,只是你可能不知道是这种设计模式)

我们要向数据库中添加一系列产品,我们可以这样定义一个产品:

1
2
3
4
5
6
const product = {
pName: '娃哈哈',
pPrice: '1.00',
pTaste: '甜的',
...
}

当我们要定义多个产品时,可以复制上面的代码改一改,当然我们也可以用构造函数来实现:

1
2
3
4
5
6
7
function Product(pName, pPrice, pTaste){
this.pName = pName
this.pPrice = pPrice
this.pTaste = pTaste
}

const newPro = new Product(pName, pPrice, pTaste)

在 JavaScript 中,我们使用构造函数去初始化对象,就是应用了构造器模式

2、工厂模式

工厂模式其实就是将创建对象的过程单独封装。目的是为了实现无脑传参

2.1、简单工厂模式

互联网公司有不同工种的员工,每个工种做的事情是不同的,假设通过一个字段 type 来区分,然后还需要为每个 type 添加自定义说明来描述具体内容,通过构造器模式可能会想到:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Coder(name, age){
this.name = name
this.age = age
this.type = 'coder'
this.desc = '写代码、怼UI、怼产品经理'
}

function ProductManager(name, age) {
this.name = name
this.age = age
this.type = 'PM'
this.desc = '给程序员提需求、给UI提需求、被老板怼、被程序员怼、被UI怼'
}

如果后续我们要添加更多工种,那我们可能会交给一个函数去处理:

1
2
3
4
5
6
7
8
9
10
11
function CreateNewType(name, age, type){
switch(type) {
case 'coder':
return new Coder(name, age)
break
case 'PM':
return new ProductManager(name, age)
break
...
}
}

但是如果公司有上百个工种,那不是要写上百个类,在 switch 中写上百个case?想想设计模式的核心思想:「封装变化」,于是有了下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function User(name, age, type, desc) {
this.name = name
this.age = age
this.type = type
this.desc = desc
}

function CreateNewType(name, age, type){
let desc = ''
switch(type) {
case 'coder':
desc = '写代码、怼UI、怼产品经理'
break
case 'PM':
desc = '给程序员提需求、给UI提需求、被老板怼、被程序员怼、被UI怼'
break
...
}

return new User(name, age, type, desc)
}

这样一来是不是又简单了许多?

工厂模式总结: 工厂模式其实就是将创建对象的过程单独封装。目的是为了实现无脑传参

从构造器模式和简单工厂模式来看:构造器解决的是多个对象实例的问题,简单工厂解决的是多个类的问题

2.2、抽象工厂模式

抽象工厂是佐证“开放封闭原则”的良好素材

我们知道一部智能手机由「操作系统」和「硬件」组成,如果我们要生产手机,需要准备好操作系统硬件,所以我们可以先定一个抽象类(js中没有抽象类,这只是一个模拟):

1
2
3
4
5
6
7
8
9
10
class PhoneFactory{
// 提供操作系统的接口
createOS(){
throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
}
// 提供硬件的接口
createHardWare(){
throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
}
}

我们要准备生产一批安卓系统,高通芯片的手机,所以有:

1
2
3
4
5
6
7
8
9
10
11
// 具体工厂继承自抽象工厂
class SmartPhone extends PhoneFactory {
createOS() {
// 提供安卓系统实例
return new AndroidOS()
}
createHardWare() {
// 提供高通硬件实例
return new QualcommHardWare()
}
}

上面我们用到了 安卓系统 和 高通芯片,那如果我们要生产其他系统、其他芯片的手机呢?我们则可以将系统和硬件也抽象化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 系统抽象化
class OS {
controlHardWare() {
throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
}
}
class AndroidOS extends OS {
controlHardWare() {
console.log('我会用安卓的方式去操作硬件')
}
}
class AppleOS extends OS {
controlHardWare() {
console.log('我会用苹果的方式去操作硬件')
}
}
...


// 硬件抽象化
class HardWare {
// 手机硬件的共性方法,这里提取了“根据命令运转”这个共性
operateByOrder() {
throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
}
}

// 定义具体硬件的具体产品类
class QualcommHardWare extends HardWare {
operateByOrder() {
console.log('我会用高通的方式去运转')
}
}

class MiWare extends HardWare {
operateByOrder() {
console.log('我会用小米的方式去运转')
}
}
...

最后,我们生产手机时只需要:

1
2
3
4
5
6
7
8
9
const myPhone = new SmartPhone()
// 安装操作系统
const myOD = myPhone.createOS()
// 组装硬件
const myHardWare = myPhone.createHardWare()
// 启动操作系统(输出‘我会用安卓的方式去操作硬件’)
myOS.controlHardWare()
// 唤醒硬件(输出‘我会用高通的方式去运转’)
myHardWare.operateByOrder()

如果我们要生产新的手机,只需要扩展种类就好,不需要修改原来的PhoneFactory

1
2
3
4
5
6
7
8
class newStarFactory extends PhoneFactory {
createOS() {
// 操作系统实现代码
}
createHardWare() {
// 硬件实现代码
}
}

这么个操作,对原有的系统不会造成任何潜在影响 实现了对拓展开放,对修改封闭

数据结构与算法-数组的应用

两数求和

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素

示例: 给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1]

常规套路

两层for 循环嵌套,事件复杂度为 O(n^2)。容易导致算法超时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function(nums, target) {
for (var i = 0; i < nums.length; i++) {
for (var j = 0; j < nums.length; j++) {
if (i !== j && nums[i] + nums[j] === target) {
return [i, j]
}
}
}
};

考虑空间换时间

考虑用空间来换时间,使用一层循环,用 Map 或者 对象 来帮忙

记住:几乎所有求和问题都可以用求差问题

1
2
3
4
5
6
7
8
9
10
11
const twoSum = function (arr, target) {
const obj = {}
const len = arr.length

for (let i = 0; i < len; i++) {
if (obj[target - arr[i]] !== undefined) {
return [obj[target - arr[i]], i]
}
obj[arr[i]] = i
}
}

双指针法-合并两个有序数组

当我们看到题目中有「有序」和「数组」这两关键词时,应考虑到使用双指针法

题目:给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
说明: 初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。 你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。

示例: 输入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3
输出: [1,2,2,3,5,6]

解析:

  1. 定义两个指针,分别指向两个有序数组的最后一个元素
  2. 比较两个指针处对应的元素的大小,把较大的元素放到 nums1 的最后
  3. 如果 nums2 的长度比 nums1 长,执行前两步后,直接将剩余的 nums2 部分添加到 nums1 后面即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const merge = function(nums1, m, nums2, n) {
// 定义两个指针指向数组最后
let i = m - 1
let j = n - 1
// 定义一个值来存放 num1 的最后位置的索引
let k = nums1.length - 1

while(i >=0 && j >=0) {
if (nums1[i] > nums2[j]) {
nums1[k] = nums1[i]
i--
k--
} else {
nums1[k] = nums2[j]
j--
k--
}
}
// 如果 nums2 的长度比 nums1 长
while (j >=0) {
nums1[k] = nums2[j]
j--
k--
}
}

双指针法-三数求和

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。

给定数组 nums = [-1, 0, 1, 2, -1, -4], 满足要求的三元组集合为: [ [-1, 0, 1], [-1, -1, 2] ]

解析:
0. 先想想我们在什么情况下会考虑使用「双指针」?没错,看到关键词:「有序」、「数组」。所以我们要先创造「有序」,可使用数组的 sort() 方法。

  1. 遍历数组,每次遍历到的元素作为一个基点,以基点后一位作为左指针,以数组最后一位作为右指针
  2. 比较三个数的和:
    1. 若和为0,则输出结果,同时向中间移动左右指针
    2. 若和小于0,则说明左指针处的数太小,应向右移动
    3. 若和大于0,则说明右指针处的数太大,应向左移动
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// nums= [-1, 0, 1, 2, -1, -4] ==> [[-2,0,2]]
// nums=[-2,0,0,2,2] ==> [ [-1, 0, 1], [-1, -1, 2] ]
const threeSum = function(nums) {
let res = []
// 数组排序
nums = nums.sort(function(a,b){
return a-b
})
let len = nums.length
for (let i = 0; i < len - 2; i++) {
let l = i + 1
let r = len - 1
// 作为基点的元素重复,跳过
if (i > 0 && nums[i] === nums[i - 1]) {
continue
}
while(l < r){
if (nums[i] + nums[l] + nums[r] < 0) {
l++

// 左指针元素重复,跳过
while(l<r&&nums[l]===nums[l-1]){
l++
}
} else if (nums[i] + nums[l] + nums[r] > 0){
r--

// 右指针元素重复,跳过
while(l<r&&nums[r]===nums[r+1]){
r--
}
} else {

res.push([nums[i] , nums[l] , nums[r]])
l++
r--
// 左指针元素重复,跳过
while(l<r&&nums[l]===nums[l-1]){
l++
}
// 右指针元素重复,跳过
while(l<r&&nums[r]===nums[r+1]){
r--
}
}
}
}
return res
}

js基础——call模拟实现

call

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。——MDN

语法

1
function.call(thisArg, arg1, arg2, ...)

参数

  • thisArg

    可选的。在 function 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。

  • arg1, arg2, …

    指定的参数列表。

例子

1
2
3
4
5
6
7
8
9
var foo = {
value: 1
};

function bar() {
console.log(this.value);
}

bar.call(foo); // 1

注意两点:

  1. call 改变了 this 的指向,指向到 foo
  2. bar 函数执行了

模拟 call 实现

由上面例子,当我们调用 bar.call(foo) 的时候,call 类似将 foo 函数改造为:

1
2
3
4
5
6
7
const foo = {
value: 1,
bar: function() {
console.log(this.value)
}
}
foo.bar(); // 1

但是,实际上,我们是不可能改变 foo 函数的,所以我们可以给 foo 先添加属性,执行完后在删除属性,于是有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Function.prototype.myCall = function(contenx) {
// context值为 foo,即 {value: 1}
// this值为 bar(){console.log(this.value)}
context.fn = this
context.fn()
delete context.fn
}

// 测试一下
var foo = {
value: 1
};

function bar() {
console.log(this.value);
}

bar.myCall(foo); // 1

由于 call 函数还能接收不定个数的参数,所以改造下上面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Function.prototype.myCall = function(contenx) {
// context值为 foo,即 {value: 1}
// this值为 bar(){console.log(this.value)}
context.fn = this
var args = [...arguments].splice(1,arguments.length) // <====注意这一步添加了这里
context.fn(...args) // // <====注意这一步修改了这里
delete context.fn
}
// 测试一下
var foo = {
value: 1
};

function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}

bar.myCall(foo,'brand', 28);
// brand
// 28
// 1

当call的第一个参数传 null 或者 undefined 的时候,this 指向全局对象,所以修改下上述代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Function.prototype.myCall = function(contenx) {
context = context || window // <====注意这一步添加了这里
// context值为 foo,即 {value: 1}
// this值为 bar(){console.log(this.value)}
context.fn = this
var args = [...arguments].splice(1,arguments.length)
context.fn(...args)
delete context.fn
}
// 测试一下
var foo = {
value: 1
};
var value = 2

function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}

bar.myCall(foo,'brand', 28);
// brand
// 28
// 2

另:function.call(thisArg, arg1, arg2, ...) 是可以有返回值的,显然我们上面的代码没有返回值。。,再来改造下,

这也是最终版了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Function.prototype.myCall = function(contenx) {
context = context || window
// context值为 foo,即 {value: 1}
// this值为 bar(){console.log(this.value)}
context.fn = this
var args = [...arguments].splice(1,arguments.length)
var res = context.fn(...args) // <====注意这一步修改了这里
delete context.fn
return res // <====注意这一步修改了这里
}
// 测试一下
var foo = {
value: 1
};
var value = 2

function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
// 注意这里添加了一个return
return {
name,
age
}
}

bar.myCall(foo,'brand', 28);
// brand
// 18
// 2
// {
// name: 'brand',
// age: 28
// }