臭脾气码农


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

面试算法入门-链表

发表于 2020-07-21 | 分类于 算法入门

写在前面

面试准备算法篇之一。链表。


Hash算法

链表(Linked List)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。

由于不必须按顺序存储,链表在插入的时候可以达到 O(1)O(1) 的复杂度,比另一种线性表 —— 顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要 O(n)O(n) 的时间,而顺序表相应的时间复杂度分别是 O(log\ n)O(log n) 和 O(1)O(1)。

使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。

在计算机科学中,链表作为一种基础的数据结构可以用来生成其它类型的数据结构。链表通常由一连串节点组成,每个节点包含任意的实例数据(data fields)和一或两个用来指向上一个/或下一个节点的位置的链接(links)。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的访问往往要在不同的排列顺序中转换。而链表是一种自我指示数据类型,因为它包含指向另一个相同类型的数据的指针(链接)。

链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表,双向链表以及循环链表。

链表通常可以衍生出循环链表,静态链表,双链表等。对于链表使用,需要注意头结点的使用。


经典题目(leetcode)

给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。

1
2
3
4
5
6
7
8
9
给定两个数组,编写一个函数来计算它们的交集。

示例 1:
输入: 1->1->2
输出: 1->2

示例 2:
输入: 1->1->2->3->3
输出: 1->2->3

解题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var deleteDuplicates = function(head) {
let flag = head
while(flag && flag.next) {
if(flag.val === flag.next.val) {
flag.next = flag.next.next
}else {
flag = flag.next
}
}
return head
}

解题思路

遍历链表,从当前节点向后遍历,删除相同val值的元素。达到去重的目的。

奇偶链表

1
2
3
4
5
6
7
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。

请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。

示例:
输入: 1->2->3->4->5->NULL
输出: 1->3->5->2->4->NULL

解题

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
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var oddEvenList = function(head) {
if (!head || !head.next) return head;
const oddHead = {
next: head
}
const evenHead = {
next: head.next
}
let curOdd = oddHead.next
let curEven = evenHead.next
while(curOdd && curEven && curEven.next && curOdd.next) {
const oddNext = curOdd.next.next
const evenNext = curEven.next.next
curOdd.next = oddNext
curEven.next = evenNext
curOdd = oddNext
curEven = evenNext
}
curOdd.next = evenHead.next
return oddHead.next
}

解题思路

首先定义两个链表头, 奇偶链表头。奇偶链表头分别引用链表第一,第二个节点。再以奇偶链表头为基础节点开始遍历。

动态将节点的cur.next.next指针赋给cur.next。同时继续遍历。

当当前节点.next不存在的时候则节点遍历结束。将偶数节点头赋到奇数cur节点。即完成了奇偶节点的分割拼接。

面试算法入门-Hash

发表于 2020-07-20 | 分类于 算法入门

写在前面

面试准备算法篇之一。Hash表。


Hash算法

哈希表(Hash Table,也叫散列表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做哈希函数,存放记录的数组称做哈希表。

一个通俗的例子是,为了查找电话簿中某人的号码,可以创建一个按照人名首字母顺序排列的表(即建立人名 xx 到首字母 F(x)F(x) 的一个函数关系),在首字母为 WW 的表中查找 “王” 姓的电话号码,显然比直接查找就要快得多。这里使用人名作为关键字,“取首字母” 是这个例子中哈希函数的函数法则 F()F(),存放首字母的表对应哈希表。关键字和函数法则理论上可以任意确定。

哈希表是使用 O(1)O(1) 时间进行数据的插入删除和查找,但是哈希表不保证表中数据的有序性,这样在哈希表中查找最大数据或者最小数据的时间是 O(N)O(N) 实现。


经典题目(leetcode)

两数之和

1
2
3
4
5
6
7
8
9
给定一个整数数组 nums 和一个目标值 target,
请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。


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

解题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function(nums, target) {
let hashMap = {}, res=[]
nums.some((num, index)=>{
if(hashMap[target-num] !== undefined) { // 使用some可以减少遍历次数
res = [index, hashMap[target-num]]
return true
}
hashMap[num] = index // 后负值可以不用担心角标重复
})
return res
};

解题思路

Hash表主要是做映射解题思路。对于该题来说。我们定义一个hashMap映射集合。判断当我们映射集合中存在target-num(当前角标值)值的元素的时候。即算找到正确解。

需要注意的是遍历时用some,当return为true时自动跳出。先判断是否有符合条件的值,再给hashMap负值可以免去当前角标重复判断。


两个数组的交集 II

1
2
3
4
5
6
7
8
9
给定两个数组,编写一个函数来计算它们的交集。

示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2,2]

示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[4,9]

解题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
*/
var intersect = function(nums1, nums2) {
let hashMap1 = {}, res = []
nums1.forEach((num)=> {
hashMap1[num] ? hashMap1[num]++ : hashMap1[num] = 1
})
nums2.forEach((num)=> {
if(hashMap1[num]) {
res.push(num)
hashMap1[num] --
}
})
return res
};

解题思路

定义hashMap存下nums1中的数字出现的次数。在nums2遍历的时候动态删除,同时在res数组中添加相应的数字即可。

面试准备

发表于 2020-07-17 | 分类于 职业成长杂谈

写在前面

入职平安2年。深陷业务无法自拔。又到了换工作的准备期。而且这次目标是一线大厂。所以记录一下准备面试的过程记录。

1。JS基础知识。JS基础在前几年的文章中已经吧难点基本写的比较清楚了。不过后续会拿面试题来提升理解。暂时先放过JS基础。

2。算法方向。总结算法方向。在本文解释大体算法类型。

3。项目工程化方向。微前端,构建工具。

4。业务方向。前端代码生成器,页面解释器,等等新颖业务解析。

5。网络层方向,前端安全性,前端性能优化,http请求,nginx。

6。node方向以及服务器渲染。koa,express。

7。运维方向。docker。


JS基础知识

把JS基础提到第一个去准备是因为JS基础真的很重要。不是一般的重要。不过难点和要点也不是太多。后续JS基础准备也是通过刷题来巩固知识。当然不排除再刷一遍JS书。也是一个不错的提升途径。


算法方向

算法是有分类的。面试一般会从各个大类挑选出1-2个方向考察面试者对算法的了解。不过最近貌似各个大厂都加强了算法的考察。不过不重要。方向总是没有变的。

1。Hash相关
2。链表操作
3。双指针遍历(滑动窗口)
4。快慢指针遍历
5。区间合并
6。字符串操作
7。数字操作
8。栈相关
9。堆相关
10。递归
11。分治法/二分法
12。动态规划
13。回溯法
14。树的遍历
15。二叉树搜索


项目工程化

但凡入行两年,还停留在业务层面,没有了解一个项目如何自动化构建,部署,发布以及到线上正常访问。都不能算是一个合格的程序员。无论前后端。至少得知道一种构建工具。并熟练的使用构建工具构建一个跟日常开发相近的前端工程。

当熟练使用一种构建工具之后。就需要在日常项目中去深入理解如何使用构建工具来提升开发效率、项目痛点以及性能优化。通过工具让coding更美好。


业务方向

现在的公司是从事人事系统管理的。主要还是2b的业务。因为有特定的交付压力。面对不同的客户会有不同的需求。但是我们公司的标准产品规划并没有很明确。所以疲于业务。

解决相关业务问题。个人总结两个方向。代码解释器(cms)和代码生成器。

cms适用于简单配置页面,当页面过于复杂时cms会存在解释器性能问题。但是用于配置一些简单的页面cms真的很方便。

而代码生成器在2b业务中会非常适用。而且便于二次开发。后续会有一篇文章来说明代码生成器的实现原理。

而2b的产品在交付时会有不同的模块。各个模块又是独立的工程。如何把这些工程结合在一起。微前端的出现又解决了相应的痛点。


网络层面,性能优化,以及nginx

这个方向主要解决的是项目上线过程中遇到的问题。作为一个合格的前端。你必须得知道开发结束之后。你的工程如何在服务器上运行。并且你构建的项目有没有可以优化的地方,以及会遇到的问题。


node

前端的趋势。虽然没有使用过,但是你必须知道如何使用。包括webpack中使用的express,当下中间键比较火的koa。也需要了解以及实践。


运维以及docker

还是哪句话,作为一个合格的开发,你必须知道你的项目如何在服务器上运行。且需要知道服务器如何操作使用,容器docker是个什么东西。


写在后面

明确了学习方向。我们就来逐一进行重点解析。争取在今年将以上的重点全部梳理一遍。

把大象装进冰箱-面向对象编程

发表于 2018-05-15 | 分类于 js进阶

背景知识

从编程懵懂期,大学老师就在讲台上强调面向对象,面向对象巴拉巴拉。从业至今写的代码并没有把这种编程思维展现的淋漓尽致,这篇文章开始梳理js中的设计模式,梳理一下多年以来的编程思路。


面向过程和面向对象

这个概念本人是从毕向东毕老师的java入门开始理解的,毕老师说了一个非常形象生动的例子,即把大象装进冰箱:

面向过程:冰箱.open() -> 冰箱.put(‘大象’) -> 冰箱.close()

面向对象:冰箱.push(‘大象’)

面向过程的思路是亲力亲为,我们开门,放进去,关门,中间的过程我们一清二楚。而面向对象的观念是冰箱你提供给我一个api,这个api可以放进去东西,具体你是开门还是锯门,i dont care,我只需要知道我调用你的api即可把我的大象放进去。


js中的面向对象

js基础系列一共写了4篇文章,分别是对象类型,原型链,this和闭包。个人感觉把js中的难点和重点都梳理了一遍,也是为js进阶做铺垫。虽说基础决定代码质量,但是设计模式和编程思路才是过关斩将的工作利器,有好体格更要有强大的武器。后面的代码就不会去过多的解释一些其中的原理,有什么晦涩难懂的地方尽量去翻看一下之前的文章,基本都有说明。

js对象

都知道js中函数和对象是互相继承的,因为之前的文章有详细介绍过彼此的关系,所以我们这里把函数对象和对象归为一类统称为对象。

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
let Car = function () {}

Car.prototype = {
dirve: function () {
console.log('开车')
return this
},
turnOnLight: function () {
console.log('开灯')
return this
},
turnOffLight: function () {
console.log('关灯')
return this
},
carAdaptations: function (name, fn) {
this[name] = fn
return this
}
}

let car = new Car()

car.dirve().turnOnLight().turnOffLight().carAdaptations('jump',function () {
console.log('车翻了')
return this // <- 注意return this
}).jump().turnOffLight() // 开车,开灯,关灯,车翻了,关灯

我们定义了一个车对象,该对象有默认4个方法,分别是开车,开灯,关灯和改装。我们在所有方法中包括通过改装的jump方法中都返回了this即Car对象本身所以我们实现了Car对象的链式调用。

看到这里。我们是不是发现这种调用方式非常的熟悉,很像下面的代码:

1
2
3
4
5
6
7
8
9
10
$.fn.extend({
hello:function(){
console.log('hello');
return this // <- 注意 return this
}
});

$('html').css('color','#ffffff').hello().css('font-size','30px').on('click', function () {
console.log(111)
}).css('background-color', '#000000')

对,就是我们jquery的调用方式。虽然jquery的fn.extend方法不像我们carAdaptations方法一样直接挂在原型上,但是实现原理是一样的。当然别人比我们考虑的要多得多!

注:在添加自定义方法的时候切记return this,否则无法加入链式调用大军。因为只有return this才能在函数执行完成拿到对象本身继续进行下一次调用,如果return this不存在那么在该函数执行结束后继续链式调用会报错。

封装

面向对象编程的第一步,即将我们的冰箱封装成为一个工具,冰箱提供的方法内部实现不需要我们关心,我们只需要知道冰箱提供给我们的api即可。下面我们来用车的例子来写两种不同的封装。

1.闭包实现对象封装

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
let CarClosure = (function () {
console.log('CarClosure 被实例化啦')
let carComputer = { // 私有属性
name: 'carCompouter'
}

function carComputerOpen() { // 私有构建方法
/*
* 省略代码
* */
}

function _carClosure(color, brand, country) {
this.color = color ? color : this.color
this.brand = brand ? brand : this.brand
this.country = country ? country : this.country
this.carComputer = carComputer // <- 仅用来验证是否只在内存中出现一次 私有方法和私有变量是不应该向外部提供的
this.carComputerOpen = carComputerOpen // <- 同上
}

_carClosure.prototype = {
color: 'black',
brand: 'Benz',
country: 'Germany',
}
return _carClosure
})()

let carClosure1 = new CarClosure()
let carClosure2 = new CarClosure('red')

2.通过安全模式创建的类

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
let CarSafe = function (color, brand, country) {
if (!(this instanceof CarSafe)) {
return new CarSafe(color, brand, country)
}
console.log('CarSafe 被实例化啦')
let carComputer = { // 私有属性
name: 'carCompouter'
}

function carComputerOpen() { // 私有构建方法
/*
* 省略代码
* */
}

this.color = color ? color : this.color
this.brand = brand ? brand : this.brand
this.country = country ? country : this.country
this.carComputer = carComputer // <- 仅用来验证是否只在内存中出现一次 私有方法和私有变量是不应该向外部提供的
this.carComputerOpen = carComputerOpen // <- 同上
}

CarSafe.prototype = {
color: 'black',
brand: 'Benz',
country: 'Germany'
}

let carSafe1 = new CarSafe()
let carSafe2 = new CarSafe()
let carSafe3 = CarSafe('red')

我们通过两种模式创建了两个实例化对象相同的类,第一种叫做闭包封装,顾名思义即通过闭包机制封装的对象。第二种叫安全模式封装的对象即原型封装,顾名思义无论是否通过new关键字来创建对象都是安全的。

那么通过这两种方式创建出来的对象有什么差别呢,我们统一执行一下代码:

1
2
3
4
5
6
console.log(carClosure1, carClosure2)
console.log(carClosure1.carComputerOpen === carClosure2.carComputerOpen)
console.log(carClosure1.carComputer === carClosure2.carComputer)
console.log(carSafe1, carSafe2, carSafe3)
console.log(carSafe1.carComputerOpen === carSafe2.carComputerOpen)
console.log(carSafe1.carComputer === carSafe2.carComputer)

有意思的事情来了。我们通过闭包机制实现的封装,在实例化两个对象时,carClosure只实例化了一次,而通过安全模式实现的封装每实例话一个对象carSafe即要实例话一次。于是乎我们猜想carSafe和carClosure内部私有化方法以及私有变量,carClosure中只在内存中出现一次,而carSafe则会出现多次。所以在执行验证代码中我们输出了

1
2
3
4
console.log(carClosure1.carComputerOpen === carClosure2.carComputerOpen)
console.log(carClosure1.carComputer === carClosure2.carComputer)
console.log(carSafe1.carComputerOpen === carSafe2.carComputerOpen)
console.log(carSafe1.carComputer === carSafe2.carComputer)

通过控制台输出的两个true和两个false也可以验证咱们的猜想。所以说两种封装机制在引擎内部执行的机制并不相同。各有利弊取舍看场景。

思考题来了。在写博客的时候有考虑过对闭包封装做检察长适配无new实例化,但是并没有完美解决代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let _carClosure = function (color, brand, country) {
this.color = color ? color : this.color
this.brand = brand ? brand : this.brand
this.country = country ? country : this.country
return this
}

_carClosure.prototype = {
color: 'black',
brand: 'Benz',
country: 'Germany',
}

let CarClosure = (function () {
let _this = {}
_this.__proto__ = _carClosure.prototype
return _carClosure.bind(_this)
})()

console.log(CarClosure(), new CarClosure())

虽然代码中bind的_this已经做了_carClosure的原型链继承,但是在浏览器描述中并没有体现出来,不知道是不是自执行函数还是闭包对此有影响查证半天也无果。各位看官如果有自己的理解或者说上面的代码有错误的地方欢迎指正。(可以通过个人qq联系我。在微博的关于中有个人联系方式,非常欢迎拍砖。)

继承

说到继承就有很多东西可以扯一下,因为js没有明确的类定义,所以说在js中的继承种类非常的多。我们挑几个有特点的继承方式说一下。分别是:类式继承,构造函数继承,组合继承,原型式继承和寄生式继承。我们先写一个类,然后通过不通方式来实现继承。

声明类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Car(color, brand, country) {
this.color = color ? color : this.color
this.brand = brand ? brand : this.brand
this.country = country ? country : this.country
this.type = 'car'
this.shoe = ['Michelin', 'Michelin', 'Michelin', 'Michelin']
this.changeShoe = function (count, brand) {
this.shoe[--count] = brand
}
}

Car.prototype = {
color: 'black',
brand: 'Benz',
country: 'Germany',
drive: function () {
console.log('车开了')
},
}

类式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function MyCar() {}

MyCar.prototype = new Car() //为MyCar实例化出的对象添加上Car的内置方法

var car1 = new MyCar()
var car2 = new MyCar()

console.log(car1.color, car1.brand, car1.country) // black Benz Germany 注1

console.log(car1 instanceof MyCar) // true 注2
console.log(car1 instanceof Car) // true
console.log(MyCar instanceof Car) // false
console.log(MyCar.prototype instanceof Car) // true

car2.drive() // 车开了 注3
car2.changeShoe(1, 'BRIDGESTONE')
console.log(car1.shoe) // ["BRIDGESTONE", "Michelin", "Michelin", "Michelin"]

我们来看看类式继承的实现过程和特点,拿出我们之前的Car的例子稍加改造。然后再定义个MyCar类,然后把Car的实例赋给MyCar的原型。都知道原型对象(prototype)是为该类实例话对象添加共有方法,MyCar.prototype = new Car()的作用就是为MyCar实例化出的对象添加上Car的内置方法。于是乎MyCar的实例即继承了Car的属性和方法。我们完成了一个类式继承。

通过注1,我们可以发现car1确实拿到了Car类下的所有属性。方法当然也是有的,通过注3我们就能知道。

注释2表示的是继承关系的结构。看看就行,跟我们分析的实现过程差不多。

注3得强调一下,我们通过换轮胎的通用方法修改了car2的轮胎,但是结果car1的轮胎也跟着一起变了。这是为什么。我们来分析一下:我们来看car1和car2是MyCar的实例,MyCar.prototype是Car的实例。也就是说MyCar创建的实例,下所有的实例引用的属性都指向了同一个Car实例的属性,也就是说car1和car2的引用数据类型都指向了同一个Car实例下的引用数据类型。对引用数据类型有疑问的同学请移步JS中数据类型剖析。当我们修改了car2的轮胎类型时,修改的是被MyCar实例化的Car.shoe该内存地址下的数据。所以我们car1下的数据也同时发生了改变。当然不通过内置方法修改的结果也是一样,不信的话请自行尝试。

再说我们在实例化car1和car2的时候,其实MyCar已经完成了对Car的继承。所以我们无法在实例化car1和car2的时候去设置Car的可变属性,这也是类式继承的一个缺点!

构造函数继承

1
2
3
4
5
6
7
function MyCar(color, brand, country) {
Car.call(this, color, brand, country)
}

let myCar = new MyCar('blue')
console.log(myCar) // MyCar {color: "blue", brand: undefined, country: undefined, type: "car"}
console.log(myCar.drive()) // myCar.drive is not a function

首先我们来看构造函数式的继承是什么,即通过执行父类的构造函数获取父类的共有方法,并且给父类绑定当前类下的this来实例化当前类的对象。大白话说,MyCar实例化了一个对象,并且把这个对象丢给Car,然后Car对这个对象进行了一些修改再做返回。

this和call我们也不多说了,有疑问请移步This到底是个什么东西。我们通过call方法改变了Car的this,所以Car的this已经不是拥有其默认属性的this,而是我们通过call传递给Car的MyCar的this。所以我们无法获取到Car.prototype下的属性。只能拿到Car构造函数所修改的this。这也是为什么我们能拿到type,blur。而brand和country无法获取的原因。

因为color是通过参数传递进去的,而type是Car构造函数中添加的。所以说要通过构造函数实现继承,哪必须把所有需要继承的方法和属性放在构造函数中,而子类创建的实例中,每个实例都会单独拥有一份属性和方法并且不能公用。这样代码的复用性就大大的降低了。

组合继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function MyCar(color, brand, country) {
Car.call(this, color, brand, country)
}

MyCar.prototype = new Car()

var car1 = new MyCar()
var car2 = new MyCar()

console.log(car1.color, car1.brand, car1.country) // red Benz Germany

car1.drive() // 车开了
car2.changeShoe(1, 'BRIDGESTONE')
console.log(car1.shoe) // ["Michelin", "Michelin", "Michelin", "Michelin"]

组合继承顾名思义就是类式继承和构造函数继承一起。我们通过类式继承拿到共有方法,然后每次实例实例化MyCar的时候再实例化一次Car所以car1和car2并没有指向同一个Car实例。当然他们继承的引用数据类型的属性并没有指向同一个地址。也就不会发生冲突,且继承了Car.prototype的方法。

正因为我们在类式继承时实例化了一次Car,且在每次实例化MyCar时都会实例化一次Car,所以该继承方式也是有缺点的,因为Car的构造函数会被多次调用。

原型式继承

所谓原型式继承即为:通过已有的对象创建一个新的对象,且不必创建新的对象类型。拿代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function platfrom(o) {
function PLATFROM() {
}
PLATFROM.prototype = o
return new PLATFROM()
}

var car = {
color: 'black',
brand: 'Benz',
country: 'Germany',
shoe: ['Michelin', 'Michelin', 'Michelin', 'Michelin'],
drive: function () {
console.log('车开了')
},
}

let newCar = new platfrom(car);
console.log(newCar.shoe) // ["Michelin", "Michelin", "Michelin", "Michelin"]
newCar.shoe[0] = 'BRIDGESTONE'
console.log(new platfrom(car).shoe) // ["BRIDGESTONE", "Michelin", "Michelin", "Michelin"]

和类式继承很像,只不过原型式继承更像通过流水线或者平台去copy一个产品原型。car即为产品原型,我们拿平台方法platfrom去copy产品原型可以得到无数量车来实现继承。此类模式的继承和类式继承一样在操作引用数据类型时也会发生连锁改变。具体原因跟类式继承相同。

寄生式继承

如果构造函数式继承是类式继承的升级版,那么寄生式继承则为原型式继承的升级版。上代码

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
function platfrom(o) {
function PLATFROM() {
}

PLATFROM.prototype = o
return new PLATFROM()
}

var car = {
color: 'black',
brand: 'Benz',
country: 'Germany',
drive: function () {
console.log('车开了')
},
}

function carFactory(obj) {
var o = new platfrom(obj)
o.shoe = ['Michelin', 'Michelin', 'Michelin', 'Michelin']
o.changeShoe = function (count, brand) {
this.shoe[--count] = brand
return this
}
return o
}

let newCar = carFactory(car)
let newCar2 = carFactory(car)

console.log(newCar.shoe) // ["Michelin", "Michelin", "Michelin", "Michelin"]
console.log(newCar.changeShoe(1, 'BRIDGESTONE').shoe)
console.log(newCar2.shoe) // ["BRIDGESTONE", "Michelin", "Michelin", "Michelin"]

寄生式继承可以理解为:嗯哼,我大众很厉害,有原型产品,有生产产品的平台,有MQB有PQ35,还有一汽和上汽。我把原型和平台给到工厂。工厂再添加一些自己的配件于是乎捷达和桑塔纳,朗逸和宝来,帕萨特和迈腾都问世了!

寄生组合式继承

先贴代码再BB。不以代码为基础的扯淡都是耍流氓!注意前方高能,代码又绕又复杂,请备好纸巾!

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
50
51
52
53
54
55
56
57
function platfrom(o) { // 平台
function PLATFROM() {}
PLATFROM.prototype = o
return new PLATFROM()
}

function platfromEnhance(carFactory, carModule) { // 增强平台
var p = platfrom(carModule.prototype)
p.constructor = carFactory
carFactory.prototype = p
}

function CarModule(color, brand, country) { // 产品原型
this.color = color ? color : this.color
this.brand = brand ? brand : this.brand
this.country = country ? country : this.country
}

CarModule.prototype = { // 产品原型属性
color: 'black',
brand: 'Benz',
country: 'Germany',
drive: function () {
console.log('车开了')
}
}

function Volkswagen(color) { // 造车工厂
CarModule.call(this, color)
this.platform = 'PQ35'
this.color = color ? color : this.color
this.brand = 'Volkswagen'
this.shoe = ['Michelin', 'Michelin', 'Michelin', 'Michelin']
}

platfromEnhance(Volkswagen, CarModule) // 工厂借鉴

// 不要写成Volkswagen.prototype = {} 形式。该形式为赋值形式,会覆盖CarModule的方法
// 工厂添加自定义方法
Volkswagen.prototype.changeShoe = function (count, brand) {
this.shoe[--count] = brand
return this
}

Volkswagen.prototype.getShoe = function () {
return this.shoe
}

var car1 = new Volkswagen()
var car2 = new Volkswagen('red')

console.log(car1)
console.log(car2)

console.log(car1.changeShoe(1, 'BRIDGESTONE').getShoe())
console.log(car2.getShoe())
car1.drive()

先说一下每个注释的解释,这个东西是啥,要它来干啥,有它的好处。一个漂亮的代码段当然是用更少的代码实现更美个功能。
平台:定义一个空函数,该函数仅做继承使用。在造车方面,我们可以理解成框架和底盘。
增强平台:给平台附加一些功能,从模型中拿一些功能和属性添加在平台上面。
产品原型&产品默认属性:我们产品的构造函数,添加默认属性和方法。
造车工厂:实例化量产车型的工厂。用来copy产品原型,添加自定义方法或者修改默认属性。

再说各个功能之间的关系。他们是怎么连接起来的。
先看平台和工厂借鉴。其实所有产品,除了发明者以外都属于借鉴。比如汽车这个东西当然是奔驰发明的汽车,所以其他品牌无论怎么说,说到底都属于借鉴奔驰的方法。哪怕它有自己强大的流水线,在最初的时候也是借鉴发明者的想法,思路和技术。类比一下国内的有名车企。这里没有任何指责的意思,站在巨人肩上当然可以走的更快看的更远。
在这里得多谢好多人,因为本人的博客也是借鉴各路大神的书以及文章才能写的出来的。例举几本个人觉得比较好的书大家可以看看:
《JavaScript高级程序设计》作者是(美)(Nicholas C.Zakas)扎卡斯 —— 入门必,提升必看,看一遍一遍的看。
《你不知道的javascript》【作者】[美]辛普森( Kyle Simpson ) —— 好书,闭包,this等等都讲的特别好。茅塞顿开。
《JavaScript设计模式》张容铭 —— 设计模式写的很好很生动,本篇就是从这边书梳理来的。
其他就是一些概念的整理查阅,就很多了,比如MDN、方应航方方老师等各路知乎大神。虽然没有直接提问和引用,到底还是看过你们的文章才能初窥js门径的。

好了先扯着么多,扯的有点远,但是怎么说,其实任何人都离不开借鉴。

回归正题,说我们的平台和工厂借鉴。我们在执行platfromEnhance(Volkswagen, CarModule)这句代码的时候,把车模型的原型交给平台,平台按照模型构建了一个对象框架反回给增强平台。然后优化框架让框架符合造车工厂的流水线,p.constructor = carFactory不能量产的汽车都不是好对象!当然流水线生产出的汽车得有车的默认属性和方法。即carFactory.prototype = p。

从逻辑层面我们说明的其中的关系。下面我们从代码底层描述一下这几者的关系:

梳理一下还是比较好理解的。

关于多继承

多继承其实有太多太多解决方案了,其实无非是复制或者其他方式来实现多对象继承。后面单独写一篇博客吧,再揉在一起的话篇幅太长了。

多态

多态是什么意思呢,即同一个方法会根据传入参数的不同拿到不同的结果。然后很多人就在这里想到了java中的函数重载,其实不然。函数重载虽然是通过同一个函数名调用,但是在底层的逻辑中,重载的函数并不是同一个方法。

而多态调用的是同一个函数,只是拿到的结果不同。写个简单的例子

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
function buyCar() {
let carBrandList = ['Volkswagen', 'Audi', 'BMW', 'Benz', 'Porsche']

function randomCar(count) {
let countN = count ? count : 2
return carBrandList[parseInt((Math.random() * 10) / countN)]
}

function buyCarAndMoney(money) {
if (isNaN(money)) {
throw 'type of money is error'
}
let moneyCount = money / 10000
if (moneyCount < 3) {
return '这点钱就不要凑热闹了!'
} else if ((3 <= moneyCount) && (moneyCount < 20)) {
return randomCar(10)
} else if ((20 <= moneyCount) && (moneyCount < 50)) {
return randomCar(2.5)
} else {
return randomCar()
}
}

this.buyCar = function () {
switch (arguments.length) {
case 0:
return randomCar()
case 1:
return buyCarAndMoney(arguments[0])
}
}
}

let myCar = new buyCar()

console.log(myCar.buyCar()) // 5选一随机
console.log(myCar.buyCar('3000')) // 根据金额随机

冰箱我要把大象放进去

把大象放进冰箱和冰箱我要把大象放进去的区别仅仅是思维模式的不同,这也就是面向过程编程和面向对象编程的区别。

因为在大型项目构建的时候并不是说一个人能完成的,我们需要配合,而面向对象的思维更适合这种协作式的开发,并且可移植性及扩展性更强!

而js也不是那个仅仅用验证用户名和密码的脚本语言。它可以完成更多的功能,适用更多的场景。当然我们的编程思维也需要迭代更新。让自己的代码更加健壮、优雅可移植性更强,我想是每一个程序员应有的基本素质吧。

经常使用而你却不知道它是闭包(js基础提升之四)

发表于 2018-04-10 | 分类于 js基础

背景知识

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。

这是Kyle Simpson大神在你不知道的javascript中所描述的闭包。个人觉得把闭包说的非常的贴切。记得有人说过如果你不能把闭包说到5岁孩子都懂的话说明你还不是很理解闭包。

先拿一道闭包面试题镇楼,看完理解完面试题咱们在一步一步的来说闭包。

1
2
3
4
5
6
7
8
9
10
11
function fun(n,o) { //<-fun_1
console.log(o)
return {
fun:function(m){ //<-fun_2
return fun(m,n); //<-fun_3
}
};
}
var a = fun(0); a.fun(1); a.fun(2); a.fun(3);
var b = fun(0).fun(1).fun(2).fun(3);
var c = fun(0).fun(1); c.fun(2); c.fun(3);

我们先用标注注释一下代码的几个关键变量,我们把fun分为fun_1,fun_2,fun_3姑且把三个fun当成三个变量来看。

fun_1有两个形参,分别为n,o。fun_2有1一个形参为m。fun_3有两个形参为m,n。fun_1运行时会打印o形参。fun_1运行返回{fun:fun_2}。fun_2运行返回fun_3的执行结果。而fun_3跟fun_1的内存指向地址一样也就是说他们是同一个函数,所以返回值还是{fun:fun_2}。

先看fun(0, undefined),毫无疑问console.log(o)为undefined。运行结果为{fun:fun_2}。通过console.log(fun(0))来求证一下我们的推断

bingo看来推想没错。就是着么个逻辑。那么继续执行这个返回结果

1
console.log(fun(0).fun(1))

看到这里fun_1,fun_2,fun_3的执行逻辑已经清清楚楚,我们在来分析console.log(o),它到底打出了那个变量。

在看我们传入的参数,分析参数是如何在三个fun之间传递的呢

题目看到这里,前期的分析已经全部做完,咱们在看题目的问题以及答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);
/*
* var a = fun(0, undefined); //undefined毫无异议
* a.fun(1) //同fun(0).fun(1)也就是咱们分析的fun('fun_1_n','fun_1_o').fun('fun_2_m1'),
* 只不过把fun_1_n换成了0,fun_1_o换成了undefined,fun_2_m1换成了1。结果就是undefined,0
* a.fun(2),a.fun(3)也是一样。我们会输出a的fun(0)中的0
* */
var b = fun(0).fun(1).fun(2).fun(3);
/*
* var b = fun(0).fun(1).fun(2).fun(3);
* 同fun('fun_1_n','fun_1_o').fun('fun_2_m1').fun('fun_2_m2').fun('fun_2_m3')
* 即undefined,0,1,2
* */
var c = fun(0).fun(1); c.fun(2); c.fun(3);
/*
* c = fun(0).fun(1) // undefined,0
* fun(0).fun(1).fun(2) // undefined,0,1
* fun(0).fun(1).fun(3) // undefined,0,1
* */

这道题把闭包的深度展现的淋漓尽致,如果你能直接回答出答案并且分析的非常清楚,那么恭喜你,js闭包的问题应该没有什么能难住你的了。

注意这里是回答正确并且分析清楚,分析比理解更难,很多时候你明明知道答案但是你说不出来,或者说你的解释别人听不明白这都是问题,也是本文的目的,让你口中的闭包不再晦涩难懂。


作用域,词法作用域,函数作用域以及块作用域。

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。

还是贴出咱们闭包的概念,闭包的概念是函数可以在词法作用域之外记住并访问所在的词法作用域。所以作用域是最最最重要的。如果没有作用域,那么闭包是不存在的。所以我们就一起看看什么是作用域以及形形色色的作用域。

1.作用域是一套规则,用于确定在何处以及如何查找变量。
2.词法作用域是定义在词法阶段的作用域。词法阶段即编译器对声明变量进行赋值的过程。
3.属于这个函数的全部变量都可以在整个函数的范围内使用及复用即为函数作用域。
4.js本不存在块级作用域,但是es6中增加了块作用域的概念。块作用域由{ }包括,if语句和for语句里面的{ }也属于块作用域。

借用你不知道的javascript中的一张图来形象的说明词法作用域和函数作用域。一些特例咱们就不提了,毕竟用到的是少数。

1
2
3
var i = 'abc'
for(var i=0;i<10;i++) {}
console.log(i) // 10

看看上面的代码,我们定义了一个变量i为字符串’abc’,然后不小心在for循环中使用了i,当我们回头想用i的时候,发现i已经从’abc’变成了10。于是乎bug出现。

虽然这个只是一个非常低端的问题,但是不能说它并不会发生,所以es6中增加了一个块级作用域的概念。即在两个大括号中通过es6的声明方式可以解决这种变量污染。

1
2
3
4
5
6
7
8
var i = 'abc'
for(let i = 0;i<10;i++) {}
console.log(i) // abc

{
const A = 'abc'
}
console.log(A) // A is not defined

这样做可以尽可能的避免在作用范围内复用某些变量,导致一些错误的出现。当然可以生成块作用域的方法不仅仅是es6中才有的。包括with和try/catch也可以创建块级作用域。但是我们不细说了,有兴趣的可以自行看看,关于var,let和const在上一篇博客This到底是个什么东西中有提到。


变量提升

在上篇博客This到底是个什么东西中也提到过,但是没有仔细说明。这次说到闭包也有涉及变量提升的地方,所以我们来看看什么是变量提升。

1
2
3
4
5
6
7
8
9
10
11
12
a = 2
var a
console.log(a) // 2

console.log(b) // undefined
var b

c()
console.log(c) // c is not a function
var c = function() {}

console.log(d) // d is not defined

都知道js是顺序执行的一种语言,也就是说我们写的代码会从上倒下执行。看我们的例子我们发现并不是这样。我们先给a赋值,再定义a哪a应该是没有值为undefined才对啊。我们先console.log(b)再声明b哪应该跟c一样报错才对啊。但是结果却跟我们想的完全不通。a的值变成了2,b也变成了undefined。

其实上面的a和b都牵扯到了js编译器的机制。叫做变量提升。

说到变量提升,我们就不得不说一下js源码在执行之前经历了什么。

虽说js和其他传统编译语言有区别,但是抽象来说代码编译也只是经历了三个阶段:

词法分析 -> 语法分析 -> 代码生成

穿插在这三个过程中又有三个不通的角色去处理各自相应的工作。分别是:

引擎:负责javascript程序的编译及执行。

编译器:负责语法分析及代码生成。

作用域:负责收集维护所有声明的变量,并且实施一套语法规则,确定当前执行代码对变量访问权限。

所以我们的代码会先编译再执行。也就是说我们的变量和函数在内的所有声明都会再代码被执行之前处理。处理逻辑如下

1
2
3
4
5
6
7
8
9
10
11
var a,b,c // <--变量提升

a = 2
console.log(a)

console.log(b)

console.log(c)
c = function() {}

console.log(d)

而函数声明和变量声明中又会出现一些相应的机制,比如函数优先等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
foo() // 1

var foo

foo = function () { // <-称为foo_2
console.log(2)
}

foo() // 2

function foo() { // <-称为foo_1
console.log(1)
}

foo = function () { // <-称为foo_3
console.log(3)
}

foo() // 3

我们来看看这个例子,我们定义了一个foo变量和一个foo函数,并且对foo进行了重写。看第一个foo的输出,我们第一次调用foo()函数时直接调用的是foo_1因为函数优先,函数声明直接提升。所以我们执行的是foo_1。之后foo_2对foo进行了赋值所以foo_2被执行了。foo_3说明后出现的函数声明还是可以覆盖之前所声明的函数。

作用域和闭包

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。

重要的事情说三遍,对闭包来说这个定义太贴切了。下面闭包大军来袭,我们会有不同兵种的闭包袭来,请准备好枪支弹药!

最最普通的 - 闭包小兵

1
2
3
4
5
6
7
8
9
function foo() {
var a = 2;
function bar() {
console.log(a)
}
return bar
}

foo()() // 2 <--此处产生了闭包。

咱们先看几个定义。foo,a,bar。a和bar是foo作用域内部的变量。而bar能访问foo内部的a变量。当我们执行foo()的时候拿到了foo函数的返回值bar。这个bar就是foo内部的bar()函数。然后我们直接调用实际上是在bar定义的词法作用域以外的地方执行。

闭包可以干掉引擎的垃圾回收机制。根据js引擎的垃圾回收机制,在foo执行之后会foo内部的作用域都会被销毁不再占用内存空间。而当闭包存在的时候,因为bar()拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以供以后在任何时间调用。

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。

拿出定义我们再比对bar记住了foo的作用域且在foo的词法作用域之外被执行了。所以说产生了闭包。

最常使用的闭包 - 回调侦察兵

1
2
3
4
5
6
7
function wait(message) {
setTimeout(function timmer() {
console.log(message)
},1000)
}

wait("i'm closure")

这个是一个最最简单的回调体现,我们通过setTimeout方法,传入timmer,当我们wait执行1秒之后,它内部的作用域并没有消失timmer在执行的时候依然保有wait的message变量。

当我们在使用回调的时候,无论是同步还是异步,我们都无法清晰的得知回调执行的环境以及执行的时间,所以闭包机制会保有相应作用域以及变量以供我们随时去调用。所以说只要存在回调函数,哪闭包就是存在的!

而回调函数在代码中无所不在,所以说回调才是隐藏起来的闭包侦察兵。

佩戴炸药包的循环闭包 - 爆破兵

1
2
3
4
5
for (var i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i) // 5 * 6 输出5个6
},1000)
}

在不理解闭包的时候我们会觉得我们不是应该输出1,2,3,4,5么为啥会输出5个6呢。而且还是同时输出,并没有每隔一秒输出一次。

首先我们来看。我们可以把for循环理解成立即执行完成的,因为在js引擎中for循环也就仅仅执行了几微秒。所以说我们直接执行了5次setTimeout,而这5次的setTimeout都是在一秒以后执行。所以说同时在一秒以后输出5次是合情合理的。

再看我们之前说的闭包场景。i是一个setTimeout作用域外的一个变量,且setTimeout在该作用域内,可以读取该作用域内的变量i。5次setTimout都在该作用域内,且i变量在该作用域下只有一个,所以5次setTimout打印出的都是同一个i。而这个i是什么呢,是for循环中的i,而for循坏跳出的条件是i大于5的时候。所以我们打印出了5个6。

为了可以让闭包在正确的环境爆破,所以这时候我们得带上我们的gps - 块作用域

解决方式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function () {
console.log(j) // 1,2,3,4,5
},1000)
})(i)
}

for (let i = 1; i<=5; i++) {
setTimeout(function () {
console.log(i) // 1,2,3,4,5
},1000)
}

我们用两种方式解决了闭包带来的自爆问题。用一个自执行函数添加了一个函数作用域,在函数作用域内重新定义新变量,改变该变量的作用域,可以理解为老式gps。而es6新引入的let也可以完美解决该问题,前文中和上一篇博客This到底是个什么东西中都有提到let,这里就不多说了。

闭包 + 循环就像爆破兵,如果定位不准或者说根本没有定位,往往容易带来一些问题,炸药包没有放好就在己方阵营中炸了。所以一定注意跟着相应的作用域一起使用,才能那里不爽炸那里,闭包循环爆破兵,你值得拥有!

战争核武器 - 模块化

首先我们通过一段代码理解一下什么是模块化

1
2
3
4
5
6
7
8
9
10
function module(id) {
function identify() {
console.log(id)
}
return {
identify: identify
}
}

module('实现模块化').identify()

我们通过闭包机制,把module中的所有内部方法放在了module()的返回值中,然后再调用其内部方法。这样我们就对我们的模块化方法进行了一次封装。

众所周知面向对象编程的三个特点是封装、继承和多态。通过闭包机制我们可以封装代码,通过原型和原型链我们可以实现继承,而多js天生就是无态的(弱类型语言)天生就支持多态。所以用js就是一个完美的面向对象语言。具体面向对象编程写法后面的文章会慢慢的提到。我们先干掉闭包大军再慢慢道来!

当然我们说的模块化核武器不会像例子着么水,炸了也没有多少水花。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*!
* jQuery JavaScript Library v3.2.1
* https://jquery.com/
*
* Includes Sizzle.js
* https://sizzlejs.com/
*
* Copyright JS Foundation and other contributors
* Released under the MIT license
* https://jquery.org/license
*
* Date: 2017-03-20T18:59Z
*/
(function (global, factory) {
/*
* 省略源码
* */

})(typeof window !== "undefined" ? window : this, function (window, noGlobal) {
/*
* 省略源码
* */
return jQuery;
});

拿出jquery大法,是不是也似曾相识,对jquery也是通过模块化来实现的。

写在最后

其实闭包在日常代码中真的是无处不在,因为我们对闭包的理解不清楚透彻所以说无法说出个一二三。记住定义

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。

闭包也可以随你征战码场!

this到底是个什么东西(js基础提升之三)

发表于 2018-03-20 | 分类于 js基础

背景知识

从编程入门开始学习java,就一直觉得this是一个神奇的机制,包括到现在为止,自己觉得对js中的this的理解也没到轻车熟路的地步。所以就用这篇文章跟着大家一起理解一下this它到底是个什么东西。


this的定义

this的绑定跟函数声明的位置没有任何的关系,只取决于函数的调用方式。当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在那里被调用(调用栈)、调用函数的方法、传入的参数等信息。在函数执行的过程中,this就是记录其中的一个属性。(转自你不知道的javascript上卷)


为什么要使用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
26
27
28
let willBuy = {name: 'Macan'}
let bought = {name: 'Bora'}

//例子一
function carName() {
return this.name.toUpperCase()
}

function buy() {
let buy = "I bought " + carName.call(this)
console.log(buy)
}

//例子二
function carNameWithoutThis(context) {
return context.name.toUpperCase()
}

function buyWithoutThis(context) {
let greeting = "I bought " + carNameWithoutThis(context)
console.log(greeting)
}

buy.call(willBuy)
buy.call(bought)

buyWithoutThis(willBuy)
buyWithoutThis(bought)

输出结果都是:

I bought MACAN
I bought BORA

我们列出来的只是一个简简单单买车的例子,然而在工作中不可能只传递如此简单的对象,所以当我们对象体量提升的时候使用this传递参数会比直接传递参数要爽的多。
例子一中通过this来隐式传递一个对象的引用,而例子二中就需要通过显式来传入对象。如果使用this,那么我们所写的方法的api将更加简洁以及利于复用。

说到这里我们得先说几个方法,call、apply、bind。


call、apply、bind

这三个方法都是Function的内置方法,也就是说我们定义函数的时候函数会继承这三个方法。他们的作用是重新定义该方法的this对象!

1
2
3
4
5
6
7
8
9
10
11
12
let obj = {
name: 'wx',
job: 'teacher',
say: function (age, sex) {
console.log(this.name + ' ' + this.job + ' ' + age + ' ' + sex)
},
}

obj.say('27', 'female') //wx teacher 27 female
obj.say.call({name:'wxy', job: 'programmer'}, '26', 'male') //wxy programmer 26 male
obj.say.apply({name:'wxy', job: 'programmer'}, ['26', 'male']) //wxy programmer 26 male
obj.say.bind({name:'wxy', job: 'programmer'}, '26', 'male')() //wxy programmer 26 male

call和apply的区别在于传入参数形式不通,call以函数参数形式传递参数,第一参数为this,从第二个参数开始对应say型参。而apply将say的型参作为一个数组形式传入。而bind和call的区别在于call返回函数执行结果而bind返回绑定this的函数。


默认绑定

如果说call,apply,bind是显示绑定this,或者说硬绑定this的话。哪我们执行方法时默认绑定的this就是默认绑定。

1
2
3
4
5
6
7
8
9
10
function foo() {
console.log(this)
}

function fooStrict() {
'use strict'
console.log(this)
}
foo() // Window
fooStrict() //undefined

在非严格模式下foo调用的时候应用了this的默认绑定,所以this指向了全局即Window,而在严格模式下禁止this关键字指向全局对象,所以fooStrict的this为undefined。


隐式绑定

前文说过,this取决于函数的调用方式所以当我们调用一个函数时,通过不同的方式调用,即使该函数在堆内存中地址没有改变,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
26
27
28
29
function foo() {
console.log(this.a)
}

function doFoo(fn) {
fn() //<--注释,调用位置
}

var a = 'window' //<--注意这里必须用var,否则当this指向window时this.a会undefined

let obj2 = {
a: 'obj2',
foo: foo
}

let obj1 = {
a: 'obj1',
foo: foo,
obj2: obj2
}

let bar = obj1.foo

foo() //window
obj1.foo() //obj1
bar() //window
obj1.obj2.foo() //obj2
doFoo(obj1.foo) //window
setTimeout(obj1.foo) //window

看看我们的例子我们一个一个分析:

1.foo(),非严格模式下this指向window,没有异议好理解。
2.obj1.foo(),我们在调用foo()时,obj1为函数foo的引用函数,所以说在调用obj的时候foo被其拥有或者包含,因此foo的this被绑定到了obj上面。所以this.a = obj1.a。
3.bar(),虽然bar = obj1.foo,但是我们知道对于引用数据类型来说,任何的赋值都是对堆内存地址的指向,所以bar引用的是foo的地址,只是把地址传递给bar的不是foo而是obj1.foo而已。
4.obj1.obj2.foo(),链式引用中最后一层会覆盖调用位置所以这里的this为obj2
5.doFoo(obj1.foo),回调引用时obj1.foo的调用位置为doFoo中的fn()如代码段中注释位置,则foo的this为doFoo的this。而doFoo的this在非严格模式下指向了window。
6.setTimeout(obj1.foo),原理和5其实是一样的,我这里单独把setTimeout拿出来说就是强调一下,为何我们在使用setTimeout方法中我们经常会去bind(this)。

var、let和const

在写this的示例的时候有个小插曲,在示例里面在定义全局a的时候注释里面强调必须用var,而不是let。因为使用let时,this.a会undefined。这里我们来顺道解释下let、this和const的区别。

很多人都说const是常量,其实这个定义严格来说并不正确对于赋值类型数据来说const就是一个常量,不可以被更改。但是对于引用数据类型来说const不能被更改的仅仅是引用的地址,即使引用地址里面的值是否改变const并不能控制。

1
2
3
4
5
const obj = {a:1}
obj.a=2
obj.b=2

console.log(obj) //{a: 2, b: 2}

现在我们在来说let和var,先简单的用let和var定义两个变量然后打印。

哎呦,var定义的变量直接挂在了window下,而let定义的变量并没有存在window下。哪let定义的a去了那里呢?

我们给上面的代码加一个debugger来看看情况

我们看到了一个Scope,这个Scope可以理解成angular中的Scope即作用域,看到浏览器解析的a和b,我们就会发现a存在了一个叫做Script的域中,而这个Script域竟然跟Global即window是平级的。

接着我们继续输入

我们发现被let声明过的变量无法进行二次声明,且被var声明过的变量也无法用let进行声明,只有var能再次声明var声明过的变量。

关于变量提升及取值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var a = [], b = [];
for (var i = 0; i < 3; i++) {
a[i] = function () {
return i
};
}

for (let j = 0; j< 3; j++) {
b[j] = function () {
return j
}
}

a.map((value) => {
console.log(value(), 'a')
})

b.map((value) => {
console.log(value(), 'b')
})

首先先说说我们的示例,这个示例是我在网上看的,觉得这个非常的鸡贼。数组里面存的不是值而是一个function,通过function来获取变量的值。按理说我们通过function值获取的变量应该是我们在执行function的时候拿到的变量的值,正如我们this一样指向取决于执行的环境。

当然用var定义的变量说明了这个思路是正确的,然而let定义的变量却让我们的思路无法自圆其说。于是无比尴尬的我通过浏览器的解析结果找到了其中的猫腻。正如上图,a数组的函数在执行的时候,只有一个作用域即global所以a数组的函数执行的结果都是3,而b数组的函数在执行的时候有两个作用域,Block作用域每次的j值都会重新初始化一次。

代码理解如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var a = [], b = [];
for (var i = 0; i < 3; i++) {
let c = i
a[i] = function () {
return c
};
}

for (let j = 0; j< 3; j++) {
let c = j
b[j] = function () {
return c
}
}

a.map((value) => {
console.log(value(), 'a')
})

b.map((value) => {
console.log(value(), 'b')
})

总的来说let相当于加持了执行环境的var,可以类比于this,在隐式绑定中,obj1.obj2.foo()结果为obj2的情况我们就知晓,其实foo中的this其实也是被obj2所加持的,所以this指向了obj2。let也是如此,也会被自己声明以及定义的环境影响套上一个运行环境的作用域。


new绑定

首先我们说一下什么是new,mdn说明如下:

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

也就是说我们通过new可以获取一个实例,这个实例可能是内置对象的实例或者自定义对象的实例。哪我们来看看用new和不用new有什么区别。

1
2
3
4
5
6
7
8
9
10
function foo(a) {
this.a = a
return this
}

var bar = new foo(2)
console.log(bar)

var test = foo(2)
console.log(test)

咦,这是什么鬼,为啥就差3个字母,能差出着么多。首先我们来分析一下通过new调用和直接调用的this是什么,直接调用的话this指向window所以test是window这个很好解释。而new创建的是一个自定义对象的实例,也就是说bar是foo的实例。所以我们拿到了一个foo的obj。既然这样哪我们强制给test绑定一个空对象,那么test就应该是一个对象而不是window了吧。

1
2
3
4
5
6
7
8
9
10
function foo(a) {
this.a = a
return this
}

var bar = new foo(2)
console.log(bar)

var test = foo.call({},2)
console.log(test)

再看下去发现bar出来的是继承foo的实例,而test仅仅是一个对象。那么这时候我们再让test继承一下foo看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(a) {
this.a = a
return this
}

var bar = new foo(2)
console.log(bar)

var test = {}
test.__proto__ = foo.prototype
test = foo.call(test,2)

console.log(test)

这时候我们在看test和bar。他们就是一样的两个实例了。所以我们总结一下:
在使用new关键字来实例化对象时,会构造一个新的对象,然后该对象继承foo,并且把this绑定到该对象,。正如我们直接执行foo时所做的三件事。


绑定优先级

我们一共说了4种this的绑定方式分别为默认绑定,隐式绑定,显示绑定以及new绑定。哪当多种绑定同时存在时我们的this该何去何从,我们写出了如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let foo = function () {
return this
}

let obj = {
a: '隐式绑定',
foo: foo
}

let bar = foo.bind({a:'显示绑定'})

console.log(obj.foo()) //隐式绑定覆盖默认绑定
console.log(obj.foo.bind({a:'显示绑定'})()) //显示绑定覆盖隐式绑定
console.log(bar()) //显示绑定覆盖默认绑定
console.log(new bar()) //new绑定覆盖显示绑定

this绑定优先级 new > bind,call,apply > 隐式绑定 > 默认绑定


箭头函数的this

什么是箭头函数,箭头函数对this有什么影响?

箭头函数表达式的语法比函数表达式更短,并且不绑定自己的this,arguments,super或new.target。这些函数表达式最适合用于非方法函数,并且它们不能用作构造函数。

从该定义中我们可以提取出以下几个观点:

1.不能用作构造函数,无法用new关键字来初始化对象。
2.this不可通过call、apply改变,且this的值取决于箭头函数定义位置,而非上下文环境。
3.没有prototype属性,arguments,super或new.target。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function foo() {
return this
}

let foo1 = () => {
return this
}

var obj = {
a: 1,
foo: foo,
foo1: foo1
}

console.log(foo.bind(obj)(), foo.call(obj)) //{a: 1, foo: ƒ, foo1: ƒ} {a: 1, foo: ƒ, foo1: ƒ}
console.log(foo1.bind(obj)(), foo1.call(obj)) //window window
console.log(obj.foo(), obj.foo1()) //{a: 1, foo: ƒ, foo1: ƒ} window

可见箭头函数的this为默认绑定的this,不存在隐式绑定,显示绑定,因为无法使用new关键字初始化,所以更不存在new绑定。


总结一下

合理使用this可以让你的代码更简洁,更优雅但是隐式绑定中有很多坑得注意。

1.对象中的函数会影响函数this指向。
2.调用函数方式会影响函数this指向。
3.回调函数会影响this指向。

总的来说存在即合理,如果不合理的话js中也不会存在this这个关键字,重要的是理解this它到底是个什么东西。

JS原型、原型链剖析(js基础提升之二)

发表于 2018-03-10 | 分类于 js基础

背景知识

看完类型开始看原型,个人觉得原型,原型链和闭包是js的两大难点,其他的知识点都是比较容易掌握的。


什么是原型、什么是原型链

JS中,每个对象都有一个内部属性prototype,我们通常称之为原型。原型的值可以是一个对象,也可以是null。如果它的值是一个对象,则这个对象也一定有自己的原型。这样就形成了一条线性的链,我们称之为原型链。


构造函数

具体概念以及如何理解请参阅JS中数据类型剖析,咱们只拿js示例来说事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Car(color) {
this.color = color
this.product = function () {
console.log("Car is producing!!!")
}
this.getColor = function () {
console.log("Car is " + color)
}
this.run = function (color) {
this.product()
console.log('Car is produced')
color ? this.getColor(color) : ''
}
return this.run(this.color)
}

let car1 = new Car()
let car2 = new Car()

console.log(car1.constructor === Car) //true
console.log(car2.constructor === Car) //true
car1和car2都是Car实例化出来的对象,所以实例的对象的constructor指向Car


原型对象

在JS中,每个函数对象都有一个prototype属性,这个属性指向函数的原型对象。

还是上面的例子,我们换一个写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Car(color, brand, country) {
this.color = color ? color : this.color
this.brand = brand ? brand : this.brand
this.country = country ? country : this.country
return this.product(this.color, this.brand, this.country)
}

Car.prototype.color = 'default'
Car.prototype.brand = 'Volkswagen'
Car.prototype.country = 'Germany'
Car.prototype.product = function(color, brand, country) {
console.log('Car('+ brand +',' + country + ','+ color +') is produced')
}

new Car() //Car(Volkswagen,Germany,default) is produced
new Car('red', 'Ferrari', 'Italy') //Car(Ferrari,Italy,red) is produced

这里我们得到第一个结论,Car实例化的时候有一些默认属性,如果我们不通过自定义的复写方法来修改的话,我们实例化的对象都会具有该属性。这些属性的集合叫做原型对象,如下:

1
2
3
4
5
6
7
8
Car.prototype = {
color: 'default',
brand: 'Volkswagen',
country: 'Germany',
product: function(color, brand, country) {
console.log('Car('+ brand +',' + country + ','+ color +') is produced')
}
}

我们生成了一个造车工厂,该工厂会生产德国产默认颜色的大众汽车,当然也可以生产其他品牌国家以及不同颜色的汽车。除了color,brand,country以及product方法以外,我们的造车工厂还有一个默认的属性:constructor。所以我们的Car.prototyep也有一个constructor属性,我们上节讲过constructor是指向它父级的一个指针所以我们来尝试一下:

1
console.log(Car.prototype.constructor === Car) //true

该结果验证了我们的结论,同我们上节讲的对象类型一样,我们在生成新的对象的时候也会添加一个constructor(构造函数)属性,这个属性指向生成它的父对象即Car,那么我们再实例化一个vwCar:

1
2
let vwCar = new Car()
console.log(vwCar.constructor === Car) //true

结论也是true,那么我们对比一下两行代码:

1
2
Car.prototype.constructor === Car
vwCar.constructor === Car

vwCar.constructor是Car方法实例化的,自然,vwCar.constructor === Car。那么通过Car.prototype.constructor === Car是不是验证了,Car.prototype也是Car实例化来的呢。即:

1
2
var car = new Car()
Car.prototype = car

原型对象(Car.prototype)是构造函数(Car)的一个实例。


__proto__

JS在创建对象的时候,除Object.prototype对象以外,所有对象的__proto__都指向创建这个对象的constructor的prototype。
上篇文章js中的数据类型中分析过Object.prototype.__proto__指向了null。

1
2
3
4
let vwCar = new Car()
console.log(Car.prototype.constructor === Car) //true
console.log(Car.prototype === vwCar.__proto__) //true
console.log(vwCar.constructor === Car) //true

通过上面代码得出如下结构图:

es5中并不是所有浏览器都支持__proto__属性,__proto__已经添加至es6标准中。


Prototyep

Prototype是保存着它们所有实例方法的真正所在。

正如我们上节所分析Object实例所有共有方法都是存在在Object.prototype中。
本节我们通过一个简单的类型Number来分析prototype。

console.log(Number.prototype)

上图中Number的内置方法多多少少都不陌生。比如

1
2
let a = 123.3213
console.log(a.toFixed(2))

这里的toFixed方法就是从Number继承来的,而toFixed方法写在Number.prototype。

(3).plus(5)如何让该语句执行结果输出一个8

该题是在面试中遇到的,当时面试官打开电脑问我(3).plus(5)是啥,我想了想没有这个方法,肯定是报错呀。毕竟没有这个方法。又问如果希望它能输出一个8该如何处理。你可以在浏览器里面输入任何代码。

首先我们来看。3是一个数字类型的数据。继承了Number下的所有方法,所以我们只需要在Number.prototype下定义一个plus方法即可。

1
2
3
4
Number.prototype.plus = function(number) {
return this + number
}
console.log((3).plus(5)) // 8

当然在工作中我们不鼓励着么做,因为Number是系统内置对象,虽然我们可以修改它的内容,但是我们没有权利去修改任意系统内置对象,因为可能会导致其他人在使用该对象的时候出现一些莫名奇妙的bug。

比如我在说明==是一个坑的时候举过的一个例子:

1
2
3
4
5
6
7
8
var x = 1
var obj = {
valueOf: function(){
x = 2
return 0
}
}
console.log(obj == 0, x) // true, 2

这里也是一样,我们复写了obj下的valueOf方法,结果是非常可怕的。如若我们复写了Object.prototype的内置方法,所有Object类型的数据内置方法都会出问题,结局可想而知。


总结升华

原型和原型链是JS实现继承的一种模型。
原型链的形成是真正是靠__proto__而非prototype
引用网上某兄台的总结

再写一个现阶段能理解的js类

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
function Car(color, brand, country) {
this.color = color ? color : this.color
this.brand = brand ? brand : this.brand
this.country = country ? country : this.country
}

Car.prototype.color = 'black'
Car.prototype.brand = 'Benz'
Car.prototype.country = 'Germany'

function Volkswagen() {
this.platform = 'PQ35'
this.brand = 'Volkswagen'
}

function MLB() {
this.platform = 'MLB'
}

function Macan() {
this.name = 'Macan'
this.type = 'suv'
this.brand = 'Porsche'
}

Volkswagen.prototype = new Car()
MLB.prototype = new Volkswagen()
Macan.prototype = new MLB()

let myCar = new Macan()

console.log(myCar.brand, myCar.name, myCar.type, myCar.platform, myCar.color, myCar.country)
console.log(myCar)

奔驰发明了汽车,大众学习了奔驰的造车方法,然后创建了MLB平台,而我买了一辆基于MLB平台叫做macan的suv,是大众的子品牌保时捷

可以看出,macan继承了MLB平台,MLB平台继承了Volkswagen,而Volkswagen继承了Car方法。示例有些偷懒,应该有保时捷出现在继承中。想想篇幅算了,后面会在js设计模式中好好写一下吧。

上图中的继承均是通过原型链来继承的。该继承方式正如上文引用:
原型和原型链是JS实现继承的一种模型
原型链的形成是真正是靠__proto__而非prototype

object其他的内置方法也可以通过该示例来进行检测,这里我就不贴代码了。各位自己试试可以加深对内置对象的理解。

JS中数据类型剖析(js基础提升之一)

发表于 2018-03-07 | 分类于 js基础

背景知识

大型js基础系列文章开更,自己深入理解js的同时并书写记录方便他人,予人玫瑰手有余香!


什么是数据类型,js中的数据类型

什么是数据类型?很多人会心一笑,说数据类型就是数据的类型啊,还有什么其他更深层层次的解释吗?
一开始我也着么认为,抱着肯定的态度打开浏览器确认一下自己的观点。于是乎下面的一段话出现在我的眼前。

数据类型在数据结构中的定义是一个值的集合以及定义在这个值集上的一组操作。

计算机是一种二进制计算工具,只存在0和1,每一位数据可存储的数据有限,只有高电位和低电位两种情况。
拿java8大数据类型中的两种最简单的数据类型int型和bool型来说。
int型最大整数:2147483647,最小整数:-2147483648。数据占位4字节。字节即B(byte)是信息的最小单位。
bool型true和false。占位1字节。
这就是数据类型存在的意义,因为不同数据信息占位不同,为了不浪费系统空间,把不同数据类型的数据尽量压缩在最小的信息单元,来节省运行程序所带来的消耗。

说着么多各位看官觉得我扯远了,可是很多时候人就是这样,只会看见事物表面的东西,而深层次的本源忘的一干二净。其实上面的一段话对于计算机毕业的学生来说并不陌生,只是我们在日常工作中忽视了代码的本源而已,只有剥茧抽丝你才能成长!

js中的数据类型一共有6种,分别是Undefine,Null,Boolean,Number,String以及Object。
数据类型的定义不说,自行查找或者理解的直接往下看。


简单数据类型和复杂数据类型关系(也有叫原始数据类型和引用数据类型的,这里按照javascript高级程序设计书中的称呼)

先看定义(引自w3school)

简单数据类型:存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置。
复杂数据类型:存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存处。

简单数据类型一共有五种,分别为:Undefine,Null,Boolean,Number,String而复杂数据类型只有一种即为Object一张w3school的图帮你理解两者之间的区别。

代码方式理解如下:

1
2
3
4
5
let stringA = 'abc'
let stringB = stringA
console.log(stringA,stringB) //abc abc
stringA = 'bcd'
console.log(stringA,stringB) //bcd abc

因为是简单数据类型,所以在改变A的时候B的值并没有改变,因为简单数据类型是在栈内存中的赋值操作。

1
2
3
4
5
let objA = {string:'abc'}
let objB = objA
console.log(objA,objB) // {string: "abc"} {string: "abc"}
objA.string = 'bcd'
console.log(objA,objB) // {string: "bcd"} {string: "bcd"}

我们定义了一个objA, objA赋值了一个对象{string:’abc’}的地址,姑且假设{string:’abc’}的地址为0x0012ffxx,这时候我们又定义了一个objB,同时把0x0012ffxx的地址赋值给objB,所以我们第一步输出为{string: “abc”} {string: “abc”},再之后我们把0x0012ffxx地址上的原始值给改了,所以objA和objB同时发生了改变。接着往下看。

1
2
objA = {string:'efg'}
console.log(objA,objB) // {string: "efg"} {string: "bcd"}

这时候我们又干了一件事,我们把objA的赋值地址给改了,而objB则没有发生改变,所以objA和objB不同了,objB还是0x0012ffxx之前的地址,而objA指向了堆内存中的另一个地址。这个就是有时候我们需要做对象深拷贝的原因。

图片理解(自己画的质量不是特别高,理解万岁)


复杂数据类型概述(也叫引用数据类型)

复杂数据类型也就是我们说的Object对象,用w3school的话来说:

引用类型通常叫做类(class),也就是说,遇到引用值,所处理的就是对象。

其实着么说并不完全正确,因为es5中并没有明确的给出’类’的概念,而我们现在说的类只是前辈通过对其他语言类的理解和对js的摸索模拟出的一个类。即使es6中加入了class和extends也是通过现有的继承和语法糖来实现的。 原型链和继承我们会单独拿出几篇文章来写,这里说一下复杂数据类型的一些概念详解。

个人觉得js中除5种简单类型以外的类型都是Object类型,即对象。这6种对象通过typeof区分也会获得到6种结果,有意思的是这些结果并不是一一对应的,Undefine,Boolean,Number,String以及Object分别会拿到自己向对应的类型,而null则拿到了一个Object类型,可能是js底层代码认为null描述的是一个没有地址的对象类型而下的定义。

而对于function类型来说,它并不是严格意义上的一种数据类型,它只能称之为函数对象,因为函数是一种特殊的对象,为了和其他对象有所区分,所以对函数对象有了一个特殊的描述。

1
2
Function instanceof Object //true
Object instanceof Function //true

对你没有看错,这两个货用instanceof运算符算出来竟然是互相继承的关系,即你中有我我中有你!
先简单说说instanceof

instanceof 可以在继承关系中用来判断一个实例是否属于它的父类型或者祖类型(即多级继承)

1
2
3
4
5
6
7
8
function Aoo(){} 
function Foo(){}
Foo.prototype = new Aoo();//JavaScript 原型继承

var foo = new Foo();
console.log(foo instanceof Foo) //true
console.log(foo instanceof Aoo) //true
console.log(foo instanceof Object) //true

我们定义了两个函数,分别为Aoo和Foo,而Foo通过原型继承了Aoo,而foo是通过Foo new创建而来,而所有实例都是通过Object生成的,所以都为true。

这时候我们祭出一张的图来理解Function和Object的关系

通过图片我们得到以下几个结论:

1.所有实例都或直接,或间接的继承自Object.prototype,而Object.prototype的_proto_指向null。js中除了简单数据类型一切都来自Object.prototype
2.Function.prototype继承自Object.prototype但Function创造了Object
3.Object instanceof Function为true,说明Object.prototype上有一段可以生产对象的指令,名字叫函数。
4.Function instanceof Object为true,说明Function也是对象。

总之Function和Object可以用鸡生蛋和蛋生鸡的方式理解。

既然已经追溯到根源,我们在浏览器中输入如下代码,看看Object.prototype是个什么东西。

1
console.log(Object.prototype);

于是乎我们在浏览器中看到了如下几个方法:

因为篇幅的关系本文仅说明Object下的7种基本属性和方法,而get,set以及已经废弃或者非标准的方法会在原型链文章中详细说明。

constructor

返回创建实例对象的 Object 构造函数的引用。(引自MDN)

构造函数是什么,很多写前端er在写js的初期,对js中的对象和类理解不深刻。所以构造函数,函数重载的概念并不是很清楚,又或着还给了大学老师。我们先温习一下java中对构造函数的定义。java中的构造函数是一种特殊的方法,主要用来初始化对象。有些类中可能存在多个构造函数,根据入参的不同生成不同的对象。
先拿一段java代码理解一下什么是构造函数以及重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Car{
private String color;
public Car(){
product();
System.out.println("Car is produced");
}
public Car(String color){
this();
this.color = color;
getColor(color);
}
public void product(){
System.out.println("Car is producing!!!");
}
public void getColor(String color){
System.out.println("Car is " + color);
}
}

随便找一个java在线编译工具来编译执行一下代码,输入:

1
2
new Car();
new Car(blue);

控制台会分别输出:

1
2
3
4
5
6
Car is producing!!!
Car is produced

Car is producing!!!
Car is produced
Car is blue

js因为历史原因,把类,构造函数等概念弱化了,所以通过js来理解类,构造函数等概念并不容易所以我们祭出一段java来理解。
首先我们定义了一个Car类,可以理解成js中我们定义了一个Car对象,这个Car对象有一个私有变量color即车的颜色,又有一个私有方法product和getColor。
在我们new Car()的时候,java会自动寻找同名构造函数即Car(),这时候我们就先调用product()方法,生产了一辆车。
当我们new Car(blue)的时候,我们也是会去寻找同名函数Car(),然后找到可以传入String类型的重载构造函数,然后调用Car(String color),然后this()即是执行Car()构造函数,然后给私有变量color赋值为blue,然后再执行getColor方法进行喷漆操作。
所以这时候我们有了两辆车,一辆没有颜色,一辆是蓝色的。

基础概念有了,我们回归到js,js的constructor是什么呢,是我们创建Object构造函数的引用。
大白话说,比如我们在浏览器中执行:

1
2
3
new Array()
new Array(5)
new Array(1,2,3,4,5)

我们会拿到:

1
2
3
[]
(5) [empty × 5]
[1, 2, 3, 4, 5]

而拿到的三个数组的constructor则是js中Array()的方法。也就是说,[].proto.constructor指向了Array()方法。
我们传入了null,Number以及1,2,3,4,5,Array通过构造函数重载分别生成了3个数组,而这3个数组的constructor又指向了生成自己的母体的地址。而这个Array是如何下蛋的请各位看官去看看js的源码。

我们理解了constructor是什么,那么这时候我们去用js去写一个我们用java写的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Car(color) {
this.color = color
this.product = function () {
console.log("Car is producing!!!")
}
this.getColor = function () {
console.log("Car is " + color)
}
this.run = function (color) {
this.product()
console.log('Car is produced')
color ? this.getColor(color) : ''
}
return this.run(this.color)
}

new Car()
new Car('blue')

执行结果如下

1
2
3
4
5
6
Car is producing!!!
Car is produced

Car is producing!!!
Car is produced
Car is blue

是不是跟java已经很相似了。因为js在诞生的时候就是仿照java来编写的,为了易用性作者故意弱化了js类的概念。但是js发展至今日又有使用类的需求,所以我们会模拟一些类的写法,具体如何模拟会在下一篇原型链说明中表述。

剩下几个已经没有与constructor一样难以理解的方法,所以转来一些定义和示例方便大家阅读。以下概念及示例均转自MDN

hasOwnProperty

所有继承了 Object 的对象都会继承到 hasOwnProperty 方法。这个方法可以用来检测一个对象是否含有特定的自身属性;并且忽略那些从原型链上继承到的属性。

示例

1
2
3
4
5
o = new Object();
o.prop = 'exists';
o.hasOwnProperty('prop'); // 返回 true
o.hasOwnProperty('toString'); // 返回 false
o.hasOwnProperty('hasOwnProperty'); // 返回 false

该方法检测对象是否含有私有的特定方法。

isPrototypeOf

isPrototypeOf() 方法允许你检查一个对象是否存在于另一个对象的原型链上。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
function Foo() {}
function Bar() {}
function Baz() {}

Bar.prototype = Object.create(Foo.prototype);
Baz.prototype = Object.create(Bar.prototype);

var baz = new Baz();

console.log(Baz.prototype.isPrototypeOf(baz)); // true
console.log(Bar.prototype.isPrototypeOf(baz)); // true
console.log(Foo.prototype.isPrototypeOf(baz)); // true
console.log(Object.prototype.isPrototypeOf(baz)); // true

propertyIsEnumerable

每个对象都有一个propertyIsEnumerable方法。此方法可以确定对象中指定的属性是否可以被for…in循环枚举,但是通过原型链继承的属性除外。如果对象没有指定的属性,则此方法返回false。

示例

1
2
3
4
5
6
7
var o = {};
var a = [];
o.prop = 'is enumerable';
a[0] = 'is enumerable';

o.propertyIsEnumerable('prop'); // 返回 true
a.propertyIsEnumerable(0); // 返回 true

toLocaleString

返回调用toString()的结果。会有些内置对象去复写该方法如Array,Number,Date。

toString

每个对象都有一个toString()方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。默认情况下,toString()方法被每个Object对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 “[object type]”,其中type是对象的类型。

valueOf

JavaScript调用valueOf方法将对象转换为原始值。默认情况下,valueOf方法由Object后面的每个对象继承。 每个内置的核心对象都会覆盖此方法以返回适当的值。如果对象没有原始值,则valueOf将返回对象本身。


因为数据类型遇到的坑(实时更新)

Number最大值问题

记得一个金融公司内部账户项目,账户id是18位的,接口反给前端是数字类型。
查看账户详情是得需要账户id来查询的,于是乎查询死活查不到怎么看代码都没有问题。
但是传出去的查询请求都是错误的id,最后才发现超过最大值,js给你自动转化了。

==和===的问题

也是在工作中遇到的,刚做前端的时候不以为然,觉得没什么区别,后来发现天差地别。
js中存在相等运算符和严格运算符。
严格运算符会判断,是否同一类型,同一原始值和同一复合类型值。
而相等运算符都会把所有原始类型的值变为数值类型进行比较。(不要小看这个,他会把0==undefined==false变为true)。
如果这样还无法说服你放弃使用==的话请看下面的代码。

1
2
3
var x = 1;
var obj = {valueOf: function(){ x = 2; return 0 }};
console.log(obj == 0, x) // true, 2;

从野生程序员到家养程序员的进化从放弃使用==开始!

webpack + react + react-router + redux最佳实践

发表于 2017-10-07 | 分类于 框架知识

构建环境

node 6.10.3, webpack 3.6.0, react 15.6.1, react-router 2.8.1
兼容IE9及以上主流浏览器,各类双核浏览器极速模式。

写在前面

每个人都掌握形形色色的前端框架,无论技术流行与否,各自都用的非常舒爽。但是时间长了,会发现有很多小问题即不影响大局,却又如鲠在喉。作为一个前端技术人员,实在无法忍受,于是重构的想法应运而生。
重构又带来很多业务代码不兼容等问题,所以本文所写一篇webpack1.x升级至3.x的经历。

项目介绍

公司之前的项目是基于webpack 1.8.5, react15.3.2, react-router2.8.1动态路由的react框架。是我认识唯一一个从事前端超过十年的大牛构建于16年初的项目,也是我前端初窥门径的奠基石。
因为包管理的紊乱,webpack配置管理紊乱,构建年代久远无法兼容一些新的功能等关系。终于忍痛对其进行了静默升级。

webpack配置

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
let ExtractTextPlugin = require('extract-text-webpack-plugin') //样式分离插件
module.exports = function (options) { // 通过配置分离开发,测试,生产打包。
return {
devtool: options.devTool, // 配置生成SourceMaps,选择合适的选项
entry: __dirname + '/config/react.app.jsx', // 唯一入口文件
output: {
path: __dirname + '/build/', // 打包后的文件存放的地方
filename: options.bundleHash ? 'assets/bundle-[hash].js' : 'bundle.js', //bundle命名是否混淆
chunkFilename: 'assets/chunk-[id]-[chunkhash].js',//webpack code splitting chunk命名
publicPath: options.publicPath // 打包公共路径
},
module: {// 各类loaders配置
loaders: [{
test: /\.(scss)$/,
loader: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
{loader: 'css-loader', options: {module: true}},
'postcss-loader',
'sass-loader'
]
})
}]
},
plugins: options.plugins, //webpack plugins 分离,方便管理
devServer: {
historyApiFallback: true, // spa单页不跳转
inline: true, //热更
proxy: { //代理
'/api': {
target: options.apiUrl,
changeOrigin: true
}
}
}
}
}

说明:没有贴loader全部loader,只贴了一个scss分离样式的loader,根据自己的需要配置相应的loader。

dev-options配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var util = require('./webpack.util') //工具包
var pluginsConfig = require('./webpack.plugins.config') //插件管理包
var config = require('config') //node config管理相关配置

module.exports = [
require('../webpack.config.js')({
devTool: 'eval-source-map',
dropConsole: false,
publicPath: 'http://' + util.getIp + ':' + config.devPort + '/assets/',
bundleHash: false,
plugins: pluginsConfig.getDevPlugins(),
apiUrl: config.apiUrl
})
]

util工具包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var os = require('os')

function getIp () {
var interfaces = os.networkInterfaces()
for (var k in interfaces) {
for (var k2 in interfaces[k]) {
var address = interfaces[k][k2]
if (address.family === 'IPv4' && !address.internal) {
return address.address
}
}
}
return '127.0.0.1'
}

module.exports = {
getIp: getIp()
}

webpack.plugins.config配置:

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
50
51
52
let webpack = require('webpack')
let path = require('path')
let StatsPlugin = require('stats-webpack-plugin') //打包状态管理插件
let ExtractTextPlugin = require('extract-text-webpack-plugin') //样式分离插件
let OpenBrowserPlugin = require('open-browser-webpack-plugin') //自动弹出浏览器插件
let HtmlWebpackPlugin = require('html-webpack-plugin') //生成html插件
let OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin') //css压缩插件
let util = require('./webpack.util')
let getDevPlugins = function () { //开发模式插件管理
return [
new ExtractTextPlugin('main.css'),
new OpenBrowserPlugin({url: 'http://' + util.getIp + ':3000/'})
]
}

let getProdPlugins = function () { //生产模式插件管理
return [
new webpack.optimize.UglifyJsPlugin({// 压缩代码
compress: {
warnings: false,
drop_console: true
},
except: ['$super', '$', 'exports', 'require']// 排除关键字
}),
new StatsPlugin(path.join('stats.json'), {
chunkModules: true,
exclude: [
/node_modules[\\/]react(-router)?[\\/]/,
/node_modules[\\/]items-store[\\/]/
]
}),
new ExtractTextPlugin('assets/[name]-[contenthash].css',{allChunks: true}),
new OptimizeCssAssetsPlugin({
cssProcessor: require('cssnano'),
cssProcessorOptions: { discardComments: {removeAll: true } },
canPrint: true
}),
new HtmlWebpackPlugin({
filename: 'build.html',
template: path.join(__dirname, 'template.html'),
minify:false,
inject: 'body',
title: "****",
})
]
}


module.exports = {
getDevPlugins,
getProdPlugins,
}

react react-router配置

webpack打包入口即reactapp入口代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react'
import { Provider } from 'react-redux'
import ReactDom from 'react-dom'
import { Router, browserHistory } from 'react-router'
import store from '../app/redux/store'
import BaseApplication from './react.base.config/BaseApplication.jsx' //react组件基础容器

const rootRoute = { //根路由
name: 'app',
component: BaseApplication, //基础容器
childRoutes: [{ //子路由
childRoutes: [
require('../app/routers')
]
}]
}

ReactDom.render((
<Provider store={store}>
<Router history={browserHistory} routes={rootRoute} />
</Provider>
), document.getElementById('react'))

react组件基础容器代码如下

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
import React from 'react'
import { connect } from 'react-redux'
import actions from '../../app/redux/actions'
import { dispatch } from '../../app/redux/store'
require('./BaseApplication.css')

class Application extends React.Component {
constructor (props) {
super(props)
dispatch(actions.setVars('landingUrl', window.location.href))
}

render () {
return (
<div className={'noSelect'}>
{this.props.children}
</div>
)
}
}

const mapStateToProps = (state) => {
return {}
}

export default connect(mapStateToProps)(Application)

根据实际需要来做基础容器(可以写一些通用的方法)。也可以是一个空容器,即用来加载子路由组件即可。

react-router子路由配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
path: 'user',
childRoutes: [
require('./content'),
{
path: 'login',
getComponent (nextState, cb) {
require.ensure([], (require) => {
cb(null, require('./Login.jsx').default)
})
}
}
]
}

webpack会根据require.ensure来进行代码分割,把请求到的代码划分为一个chunk文件。这里webpack1.x和2.x与3.x是有区别的。webpack1.x和2.x不需要添加

cb(null, require(‘./Login.jsx’).default)
.default

而3.x必须添加.default如不添加则会找不到组件且不报错。这个坑处理了很久。

github地址

  • 项目github
xy

xy

9 日志
5 分类
26 标签
GitHub
© 2020 xy
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.3