前端面试题整理

面试题整理

算法相关

排序

function bubbleSort(array) {
	const len = array.length
	for (let i = 0; i < len - 1; i++) {
		for (let j = i + 1; j < len; j++) {
			if (array[i] > array[j]) {
				[array[i], array[j]] = [array[j], array[i]]
			}
		}
	}
	return array
}
function selectSort(array) {
	for (let i = 0; i < array.length; i++) {
		let minKey = i
		for (let j = i; j < array.length; j++) {
			if (array[minKey] > array[j]) {
				minKey = j
			}
		}
		[array[i], array[minKey]] = [array[minKey], array[i]]
	}
	return array
}
function quickSort(array) {
	const sort = function(left = 0, right = array.length - 1) {
		if (left >= right) return
		const pivot = array[left]
		let i = left
		let j = right
		while (i !== j) {
			while (i < j && array[j] > pivot) j-- // 从最右侧向左,直到找到一个比 pivot 小的元素
			while (i < j && array[i] <= pivot) i++ // 从最左侧向右,直到找到一个比 pivot 大的元素
			if (i < j) [array[i], array[j]] = [array[j], array[i]] // 交换两个元素的位置
		}
		array[left] = array[i]
		array[i] = pivot
		sort(left, i - 1)
		sort(i + 1, right)
	}
	sort()
	return array
}
function insertSort(array) {
	let prevIndex = 0
	let current = 0
	for (let i = 1; i < array.length; i++) {
		prevIndex = i - 1
		current = array[i]
		while (prevIndex >= 0 && array[prevIndex] > current) {
			array[prevIndex + 1] = array[prevIndex]
			prevIndex--
		}
		array[prevIndex + 1] = current
	}
	return array
}

写一个 mySetInterVal(fn, a, b),每次间隔 a,a+b,a+2b,…,a+nb 的时间,然后写一个 myClear,停止上面的 mySetInterVal

function mySetInterval(fn, a, b) {
	this.a = a
	this.b = b
	this.timeCount = 0
	this.handle = -1
	this.start = () => {
		this.handle = setTimeout(() => {
			fn()
			this.timeOut++
			this.start()
		}, this.a + this.b * this.timeOut)
	}
	this.stop = () => {
		this.handle && clearTimeout(this.handle)
	}
}
const m = new mySetInterval(
	() => { console.log('run') },
	500,
	200
)
m.start()
m.stop()

合并二维有序数组成一维有序数组,归并排序的思路

合并排序图解

function sortArray(array1, array2) {
	let result = []
	while(array1.length > 0 && array2.length > 0) {
		if (array1[0] <= array2[0]) {
			result.push(array1.shift())
		} else {
			result.push(array2.shift())
		}
	}
	return result.concat(array1).concat(array2)
}
function mergeArray(array) {
	return array.reduce((a, b) => {
		return sortArray(a, b)
	})
}
const arr = [[1,2,3],[4,5,6],[7,8,9],[1,2,3],[4,5,6]]
mergeArray(arr)

斐波那契数列

  • 递归
function fibonacci(n) {
	if (n < 0) return
	if (n < 2) return n
	return fibonacci(n - 1) + fibonacci(n - 2)
}
fibonacci(10)
  • 递归优化
function fibonacci(n) {
	if (n < 0) return
	if (n < 2) return n
	function _fib(m, a, b) {
		if (m === 0) return a
		return _fib(m - 1, b, a + b)
	}
	return _fib(n, 0, 1)
}
fibonacci(10)

字符串出现的不重复最长长度

图解

function lengthOfLongestString(str) {
	const array = [...str]
	const result = array.reduce((a, b) => {
		const index = a.indexOf(b)
		if (index > -1) a = a.substring(index + 1, s.length)
		return a + b
	})
	return result.length
}
lengthOfLongestString('loddktdji')
lengthOfLongestString('dvdf')
lengthOfLongestString('adfafwefffdasdcx')

有一堆整数,请将其分成三份,确保每一份总和尽量相等

/* 
   先把数组从大到小排列,同时算出平均值
   按照顺序依次向数组中push进元素,直至数组元素总和大于均值(不能大于均值)
   对于不满足上面条件的数据,依次选择元素总和最小的数组push
*/
function equalDivide(array) {
	array = array.sort((a, b) => { return b - a })
	const sum = array.reduce((a, b) => { return a + b })
	const average = Math.floor(sum / 3)
	let result = [[], [], []] // 存储分组结果
	let total = [0, 0, 0] // 存储每组元素总和
	for (let index in array) {
		let isPush = false
		for (let i in total) {
			// 当前数组元素总和 + 当前元素 < 均值
			if (total[i] + array[index] < average) {
				isPush = true
				result[i].push(array[index])
				total[i] += array[index]
				break
			}
		}
		if (!isPush) {
	      const i = total.indexOf(Math.min(...total))
	      total[i] += array[index]
	      result[i].push(array[index])
	    }
	}
	return result
}
const array = [11, 42, 23, 4, 5, 6, 4, 5, 6, 11, 23, 42, 56, 78, 90]
equalDivide(array)

对一个数字每3位加一个逗号,如输入100000,输出100,000(不考虑负数、小数)

function formateIntNumer(num) {
  num = String(num)
  const array = []
  let index = -1
  for (let i in num) {
    if (i % 3 === 0) {
      ++index
      array[index] = ''
    }
    array[index] += num[i]
  }
  return array.join(',')
}
formateIntNumer(234765342367487589)

实现超出整数存储范围的两个正整数相加

function add(a, b) {
	const len1 = a.length
	const len2 = b.length
	// 长度较短的数组首位补0直至二者长度相等
	for (let i = 0; i < Math.max(len1, len2) - Math.min(len1, len2); i++) {
		if (len1 > len2) b = '0' + b
		if (len1 < len2) a = '0' + a
	}
	// 字符串转数组并逆排序
	a = a.split('').reverse()
	b = b.split('').reverse()
	let result = []
	a.forEach((data, index) => {
		let sum = Number(data) + Number(b[index]) + Number(result[index] || 0)
		if (sum >= 10) { // 总和大于10进1
			result[index + 1] = 0
			result[index + 1] += 1
			sum = sum - 10
		}
		result[index] = sum
	})
	return result.reverse().join('')
}
add('999999999999999999999', '999999999999999999')

任意二维数组的全排列组合

function combination(array) {
	array = array.reduce((a, b) => {
		let arr = []
		a.forEach(aStr => {
			b.forEach(bStr => {
				arr.push(aStr + bStr)
			})
		})
		return arr
	})
	return array
}
combination([['A1', 'B1', 'C1'], ['A2', 'B2', 'C2'], ['A3', 'B3']])

计算出字符串中出现次数最多的字符是什么,出现了多少次?

function strMap(str) {
	let count = {}
	str.split('').forEach(i => {
		if (count[i]) {
			count[i]++
		} else {
			count[i] = 1
		}
	})
	const key = Object.keys(count).reduce((a, b) => {
		return count[a] > count[b] ? a : b
	})
	return `出现次数最多的字符是 ${key},出现了 ${count[key]} 次`
}
strMap('sdkhi.yflgvbredsuliigouphfoeijdwml;s,a')

编写一个函数 parseQueryString,用途是把 URL 参数解析为一个对象

function parseQueryString(url) {
	url = url.substring(url.indexOf('?') + 1)
	let result = {}
	url.split('&').forEach(data => {
		data = data.split('=')
		result[data[0]] = data[1]
	})
	return result
}
parseQueryString('https://editor.csdn.net/md?name=a1&articleId=117519747')

如果给定的字符串是回文,返回true,反之,返回false。回文:如果一个字符串忽略标点符号、大小写和空格,正着读和反着读一模一样,那么这个字符串就是palindrome

function palindrome(str) {
	str = str.replace(/[^0-9A-Za-z]/g, '').toLowerCase()
	const strReverse = str.split('').reverse().join()
	return str === strReverse
}
palindrome('DUIGH6$YEW8%Yw93r0#$%^&92uD.,/JHV*SFWDE')

确保字符串的每个单词首字母都大写,其余部分小写

function strMap(str) {
	const array = str.split(' ').map(s => {
		s = s.split('')
		s[0] = s[0].toUpperCase()
		return s.join('')
	})
	return array.join(' ')
}
strMap("I'm the title")

将数组扁平化并去除其中重复数据,最终得到一个升序且不重复的数组

function flatten(array) {
	// reduce 第二个参数为传递给函数的初始值
	return array.reduce((a, b) => {
		return a.concat(Array.isArray(b) ? flatten(b) : b)
	}, [])
}
function handleArray(array) {
	// array.flat(Infinity) // Infinity表示彻底扁平化,默认深度为 1
	return Array.from(new Set(flatten(array))).sort((a, b) => (a - b))
}
handleArray([0, [1, 1, 2], [3, 4], 5, 6, 7])

浏览器相关

Http 缓存策略,有什么区别,分别解决了什么问题

浏览器缓存策略

浏览器每次发起请求时,先在本地缓存中查找结果以及缓存标识,根据缓存标识来判断是否使用本地缓存。如果缓存有效,则使用本地缓存;否则则向服务器发起请求并携带缓存标识。
根据是否需要向服务器发起 HTTP 请求,将缓存过程分为两个部分:强缓存协商缓存强缓存优先于协商缓存

  • 强缓存:服务器通知浏览器一个缓存时间,在缓存时间内,下次请求直接用缓存,不在时间内,执行比较缓存策略。
  • 协商缓存:让客户端与服务器之间能实现缓存文件是否更新的验证,提升缓存的复用率,将缓存信息中的 ETag 和 Last-Modified 通过请求发送给服务器,由服务器校验,返回 304 状态码时,浏览器直接使用缓存。

HTTP缓存都是从第二次请求开始的:

  • 第一次请求资源时,服务器返回资源,并在 response header 中回传资源的缓存策略
  • 第二次请求时,浏览器判断这些请求参数,命中强缓存直接返回 200,否则就把请求参数加到 request header 头中传给服务器,命中协商缓存返回 304,否则服务器会返回新的资源
    缓存运作的整体流程图:
    缓存运作流程图

强缓存

  • 强缓存命中直接读取浏览器本地的资源,在 network 中显示的是 from memory 或者 from disk
  • 控制强缓存的字段有:Cache-Control(http1.1)和 Expires(http1.0)
  • Cache-Control 是一个相对时间,用以表达自上次请求正确的资源之后的一段时间内缓存有效
  • Expires 是一个绝对时间,用以表达在这个时间点之前可以直接从浏览器中读取数据,无需重新发起请求
  • Cache-Control 的优先级高于Expires的优先级。前者的出现是为了解决 Expires 在浏览器时间被手动更改导致缓存判断错误的问题,如果同时存在则使用 Cache-Control
Expires
  • 该字段是服务器响应消息头字段,告诉浏览器在过期时间之前可以直接从浏览器缓存中读取数据
  • Expires 是 HTTP1.0 的字段,表示缓存到期时间,是一个绝对时间(当前时间+缓存时间)。在响应消息头中设置这个字段之后就可以告诉浏览器,在未过期之前不需要再次请求
  • 由于是绝对时间,用户可能会将客户端本地的时间进行修改,而导致浏览器判断缓存失效,重新请求该资源。此外,即使不考虑修改,时差或者误差等因素也可能造成客户端与服务端的时间不一致,导致缓存失效
  • 优点
    • HTTP1.0 产物,可以在 HTTP1.0 和 1.1 中使用,简单易用
    • 以时刻标识失效时间
  • 缺点
    • 时间是由服务器发送的(UTC),如果服务器时间和客户端时间存在不一致,可能会出现问题
    • 存在版本问题,到期之前的修改客户端是不可知的
Cache-Control
  • 该字段表示资源缓存的最大有效时间,在该时间内,客户端不需要向服务器发送请求
  • 是相对时间。以下是该字段常用的值:
    • max-age:最大有效时间
    • must-revalidate:如果超过了 max-age 的时间,浏览器必须向服务器发送请求,验证资源是否还有效
    • no-cache:不使用强缓存,需要与服务器验证缓存是否新鲜
    • no-store:真正意义上的“不要缓存”,所有内容都不走缓存,包括强制和对比
    • public:所有的内容都可以被缓存
    • private:默认值,所有的内容只有客户端才可以缓存,代理服务器不能缓存
  • 优先级高于 Expires(为了兼容 HTTP1.0 和 HTTP1.1),实际项目中两个字段都可以设置
  • 该字段可以在请求头或者响应头设置,可组合使用多种指令:
    • 可缓存性
      • public:浏览器和代理服务器都可以缓存页面信息
      • private:默认,代理服务器不可缓存,只能被单个用户缓存
      • no-cache:浏览器和服务器都不应该缓存页面信息,但仍然可以缓存,只是在缓存前需要向服务器确认资源是否被更改。可配合 private,过期时间设置为过去时间
      • only-if-cache:客户端只接受已缓存的响应
    • 到期
      • max-age=:缓存存储的最大周期,超过这个周期被认为过期
      • s-maxage=:设置共享缓存,比如 can,会覆盖 max-age 和 expires
      • max-stale[=]:客户端愿意接收一个已经过期的资源
      • min-fresh=:客户端希望在指定时间内获取最新的响应
      • stale-while-revalidate=:客户端愿意接收陈旧的响应,并且在后台一并检查新的响应。时间代表客户端愿意接收陈旧响应的时间长度
      • stale-if-error=:如果新的检测失败,客户端则愿意接收陈旧响应,时间代表等待时间
    • 重新验证和重新加载
      • must-revalidate:如页面过期,则去服务器进行获取
      • proxy-revalidate:用于共享缓存
      • immutable:响应正文不随时间改变
    • 其他
      • no-store:绝对禁止缓存
      • no-transform:不得对资源进行转换和转变,例如不得对图像格式进行转换
  • 优点
    • HTTP1.1 产物,以时间间隔标识失效时间,解决了 Expires 服务器和客户端相对时间的问题
    • 比 Expires 多了许多设置选项
  • 缺点
    • 存在版本问题,到期之前的修改客户端是不可知的

协商缓存

  • 状态码由服务器决策返回 200 或者 304
  • 当浏览器的强缓存失效或者请求头中设置了不走强缓存,并且在请求头中设置了 If-Modified-Since 或者 If-None-Match,会让这两个属性值到服务端去验证是否命中协商缓存,如果命中了协商缓存,会返回 304 状态,加载浏览器缓存,并且响应头会设置 Last-Modified 或者 ETag 属性
  • 协商缓存有两组字段:Last-Modified/If-Modified-since(http1.0)和 Etag/If-None-match(http1.1)
  • Last-Modified/If-Modified-since 表示服务器资源最后一次的修改时间;Etag/If-None-match 表示服务器资源的唯一标识,只要资源变化,Etag 就会重新生成
  • Etag/If-None-match 的优先级高于 Last-Modified/If-Modified-since 的优先级
Last-Modified/If-Modified-since
  • 服务器通过 Last-Modified 字段告知客户端,资源最后一次被修改的时间,例如 Last-Modified: Mon, 10 Nov 2018 09:10:11 GMT
  • 浏览器将这个值和内容一起记录在缓存数据库中
  • 下一次请求相同资源时,浏览器从自己的缓存中找出“不确定是否过期”的缓存。因此在请求头中将上次的 Last-Modified 的值写入到请求头的 If-Modified-Since 字段
  • 服务器会将 If-Modified-Since 的值与 Last-Modified 字段进行对比。如果相等则表示未修改,响应304;反之则表示修改过,响应200并返回数据
  • 优点
    • 不存在版本问题,每次请求都会去服务器进行校验
  • 缺点
    • 只要资源修改,无论内容是否发生实质性的变化,都会将该资源返回客户端,例如周期性重写
    • 以时刻为标识,无法识别 1 秒内进行多次修改的情况。如果资源更新的速度是秒以下单位,那么该缓存是不能被使用的,因为时间单位最低是秒
    • 某些服务器不能精确得到文件的最后修改时间
    • 如果文件是通过服务器动态生成的,那么该方法的更新时间永远是生成的时间,尽管文件可能没有变化,所以起不到缓存的作用
Etag/If-None-match
  • ETag 存储的是文件的特殊标识(一般都是hash生成的),服务器存储着文件的 Etag 字段。之后的1流程和 Last-Modified 一致,只是 Last-modified 字段和它所表示的更新时间改变成了 ETag 字段和它所表示的文件 hash,把 If-Modified-Since 变成了 If-None-Match。服务器同样进行比较,命中返回 304,不命中返回新的资源和200
  • 浏览器在发起请求时,服务器返回在 response header 中返回请求资源的唯一标识。在下一次请求时,会将上一次返回的 ETag 值赋给 If-None-Matched 并添加在 request header 中。服务器将浏览器传来的 If-None-Matched 与本地资源的 ETag 作对比,如果匹配则返回 304 通知浏览器读取本地缓存,否则返回 200 和更新后的资源
  • ETag 的优先级高于 Last-modified
  • 优点
    • 可以更加精确地判断资源是否被修改,识别 1 秒内多次修改的情况
    • 不存在版本问题,每次请求都向服务器进行校验
  • 缺点
    • 计算 ETag 值需要性能损耗
    • 分布式服务器存储的情况下,计算 ETag 的算法如果不一样,会导致浏览器从一台服务器上获得页面内容后到另外一台服务器上进行验证是出现 ETag 不匹配的情况

JavaScript 相关

代码执行结果

  • 1
function side(arr) {
  arr[0] = arr[2]
}
function a(a, b, c = 3) {
  c = 10
  side(arguments)
  console.log(arguments) // [1, 1, 1]
  return a + b + c
}
a(1, 1, 1) // 12
  • 2
var min = Math.min()
var max = Math.max()
console.log(min < max) // false
  • 3
var a = 1;
(function a () { // 具名函数a
    a = 2
    console.log(a)
})()
/*
  ƒ a () {
      a = 2
      console.log(a)
  }
*/
  • 4、数值比较
    • 在 js 中,将对象转换成原始值会调用 toPrimitive() 内部函数
    • 数组从非 primitive 转为 primitive 的时候会先隐式调用 join 变为 ‘0’,String 类型和 Boolean 类型比较时,两个都先转为 Number 类型再比较
    • js 比较规则:
      • 如果比较的是原始数据类型的值,值会转成 Number 类型再比较
      • 对象与原始数据类型比较时,对象会转成原始数据类型再比较
      • undefined 和 null 与其他类型进行比较时,结果都为 false,相互比较时为 true
var a = [0]
if (a) { // Boolean([0]) -> true
  // a的转换过程 [0] -> '0' -> 1
  console.log(a == true)
} else {
  console.log(a)
}
  • 5、变量声明提升
// b 被赋值给 a,非严格模式下 b 发生了变量提升,再被赋值
(function () {
  var a = (b = 5)
})()
console.log(b) // 5
console.log(a) // Uncaught ReferenceError: a is not defined
  • 6、this指向
var fullname = 'a'
var obj = {
   fullname: 'b',
   prop: {
      fullname: 'c',
      getFullname: function() {
         return this.fullname
      }
   }
};
 
console.log(obj.prop.getFullname()) // c
var test = obj.prop.getFullname
console.log(test()) // a
var foo = {
  bar: function() {
    return this.baz
  },
  baz: 1
}
console.log(typeof (f=foo.bar)()) // undefined
const num = {
  a: 10,
  add() {
    return this.a + 2
  },
  reduce: () => this.a - 2
};
console.log(num.add()) // 12
console.log(num.reduce()) // NaN,此时this指向window
  • 7、delete + 属性继承
var company = {
    address: 'beijing'
}
var yideng = Object.create(company) // 继承了company的属性和方法
delete yideng.address // address是yideng从company继承的属性,delete删除无效
console.log(yideng.address) // beijing
  • 8、作用域
var foo = function bar(){
  return 12
}
console.log(typeof bar()) // Uncaught ReferenceError: bar is not defined

var foo = function() {
  function bar(){
    return 12
  }
  return bar()
}
var x = 1
// ()内是一个单独的作用域,该作用域内发生了变量提升,并且对f进行赋值
if(function f() {}){
  // 该作用域内
  x += typeof f
}
console.log(x) // 1undefined
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1)
}
// 1
// 2
// 3
// 闭包
var test = (function(i){
  return function() {
    console.log(i *= 2)
  }
})(2)
test(5) // 4

function box(i) {
  function inner() {
    console.log(i *= 2)
  }
  return inner
}
var test = box(2)
test(5)
var a = 0, b = 0
function A(a) {
  A = function(b) {
    console.log(a + b++)
  }
  console.log(a++)
}
// a 和 b都是先输出再加 1
A(1) // 1
A(2) // 4
var x = 2
var y = {
  x: 3,
  z: (function(x) {
    this.x *= x
    x += 2
    return function(n) {
      this.x *= n
      x += 3
      console.log(x)
    }
  })(x)
}
// x=2 -> this.x=2*2=4(this指向window) -> x=2+2=4(当前作用域中的x)
var m = y.z
// n=4 -> this.x=4*4=16(实际是window上的x) -> x=4+3=7(当前作用域中的x)
m(4) // 7
// this.x=3*5=15(this指向y) -> x=7+3=10(当前作用域中的x)
y.z(5) // 10
console.log(x, y.x) // 16 15
var x = 0, y = 1
function fn() {
  x += 2
  fn = function(y){
    console.log(y + --x)
  }
  console.log(x, y)
}
fn(3) // 2 1
fn(4) // 5
console.log(x, y) // 1 1
  • 9
function f() {
  return f
}
// new f()返回的结果为f的函数对象,并不是f的一个实例
console.log(new f() instanceof f) // false
  • 10
const person = { name: 'yideng' }
function sayHi(age) {
  return `${this.name} is ${age}`
}
console.log(sayHi.call(person, 5)) // yideng is 5
console.log(sayHi.bind(person, 5))
/*
  ƒ sayHi(age) {
    return `${this.name} is ${age}`
  }
*/
  • 11、parseInt
    • 接收两个参数 parseInt(string, radix),以第二个参数为基准解析第一个字符串参数
    • radix 为解析字符串的基数,基数规则如下:
      • 区间范围介于 2~36 之间
      • 当参数为 0,会按照十进制解析
      • 如果 string 以 0x 开头,其后的字符被解析为十六进制的整数。例如:parseInt(‘0xf’) -> 15
      • 如果 string 以 0 开头,其后的字符被解析为八进制或十六进制的数字。例如:parseInt(‘08’) -> 8
      • 如果 string 以 1~9 开头,会将其解析为十进制的整数。例如:parseInt(‘88.99f’) -> 88
      • 如果字符串的第一个字符不能被转换为数字,则返回 NaN。例如:parseInt(‘f’) -> NaN
['1', '2', '3'].map(parseInt) // [1, NaN, NaN]

/*
	parseInt('1', 0) -> 按照十进制解析,返回1
	parseInt('2', 1) -> radix = 1 不在合法范围内,返回NaN
	parseInt('3', 2) -> 二进制数字是以 0 或 1 开头,string  不合法,返回 NaN
*/
['1', '2', '3'].map((item, index) => {
	return parseInt(item, index)
})
  • 12、setTimeout、promise、async await的执行顺序
    • 遵循事件循环机制
    • 当JS解析执行时,会被引擎分为两类任务:同步任务(synchronous)和异步任务(asynchronous)
    • 对于同步任务,会被推到执行栈中按照顺序直接执行;对于异步任务,会被放到任务队列中等待执行
    • 当执行栈中所有同步任务执行完毕后,JS引擎会查看任务队列中是否有待执行任务,并将待执行任务放到执行栈中执行,执行完毕后会再次确认任务队列中是否存在待执行任务
    • 对于任务队列,被分为微任务(microtask)队列和宏任务(macrotask)队列
    • 宏任务:整段 script 代码、setTimeout、setInterval;微任务:Promise、process.nextTick
    • 微任务执行优先级高于宏任务
async function async1() {
  console.log('async1 start') // 同步任务2
  await async2() // 同 new Promise
  console.log('async1 end') // 异步任务,进入微任务队列1
}

async function async2() {
  console.log('async2') // 同步任务3
}

console.log('script start') // 同步任务1

setTimeout(function () {
  console.log('settimeout') // 异步任务,进入宏任务队列1
})

async1()

new Promise(function (resolve) {
  console.log('promise1') // 同步任务4
  resolve()
}).then(function () {
  console.log('promise2') // 异步任务,进入微任务队列2
})

console.log('script end') // 同步任务5

// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// settimeout
  • 13
let a = {}, b = '0', c = 0
a[b] = 'JavaScript'
a[c] = 'HTML+CSS'
console.log(a[b]) // HTML+CSS
let a  = {}, b = Symbol('1'), c = Symbol('1')
a[b] = 'JavaScript'
a[c] = 'HTML+CSS'
console.log(a[b]) // JavaScript
let a = {}, b = { n: '1' }, c = { m: '2' }
a[b] = 'JavaScript'
a[c] = 'HTML+CSS'
console.log(a[b]) // HTML+CSS

// 对象做属性名时会调用 toString 方法转换成字符串,转字符串结果都为 [object, Object]
// 构造函数 Foo 内部有一个指向匿名函数的属性 getName
function Foo() {
  getName = function() {
    console.log(1)
  }
  return this
}
Foo.getName = function() { // 对属性重新赋值
  console.log(2)
}
Foo.prototype.getName = function() {
  console.log(3)
}
// 当定义的变量和声明的函数重名时,函数声明提前于变量声明,最终会被变量覆盖
var getName = function() { // 匿名函数被赋值给了变量 getName
  console.log(4)
}
function getName() { // 名字为 getName 的函数
  console.log(5)
}

// 函数 Foo 的静态方法
Foo.getName() // 2
// 函数有提前声明的原则,声明后被 var getName = ... 覆盖
getName() // 4
// Foo() 返回的 this 指向 window,并产生了一个全局 getName 指针指向一个匿名函数,原 getName 被覆盖
Foo().getName() // 1
// window.getName 已经被覆盖
getName() // 1
// 成员访问 . 优先级 > new(不带参数)优先级,所以先执行 Foo.getName()
new Foo.getName() // 2
// new(带参数)优先级 > 函数调用优先级,所以先执行 new Foo()
// 构造函数实例不能访问到其静态成员,静态成员是绑定在构造函数自身上的属性方法
// var f = new Foo() -> f.getName()
new Foo().getName() // 3
// var f = new Foo() -> f.getName() -> new f.getName()
new new Foo().getName() // 3
function A() {
  console.log(1)
}
function Func() {
  A = function() {
    console.log(2)
  }
  return this
}
Func.A = A
Func.prototype = {
  A: () => {
    console.log(3)
  }
}
A() // 1
Func.A() // 1
Func().A() // 2,Func() 后 this 指向 window,全局 A 被重新赋值
new Func.A() // 1
new Func().A() // 3
new new Func().A() // 3,箭头函数不支持 new 运算符
  • 15
setTimeout(() => {
  console.log(1)
}, 20)

console.log(2)

setTimeout(() => {
  console.log(3)
}, 10)
 
console.log(4)

// for循环造成阻塞
console.time('AA')
for(let i = 0; i < 90000000; i++){
  //do somthing
}
console.timeEnd('AA')
 
console.log(5)
 
setTimeout(() => {
  console.log(6)
}, 8)
 
console.log(7)
 
setTimeout(() => {
  console.log(8)
}, 15)
 
console.log(9)

// 2
// 4
// AA: 53.68212890625 ms
// 5
// 7
// 9
// 3
// 1
// 6
// 8
  • 16

当 a 为何值时,下面的条件能成立

var a
if (a == 1 && a == 2 && a == 3) {
   console.log('条件成立')
}
// 1
var a = {
  i: 0,
  toString: function() {
    return ++i
  }
}

// 2
var a = [1, 2, 3]
a.toString = a.shift

// 3
var i = 0
Object.defineProperty(window, 'a', {
  get: function() {
    return ++i
  }
})

实现 add(1)(2)(3)

函数柯里化:柯里化(Curryimg)是把接受多个参数的函数转变为接受一个单一参数的函数,并且返回接收余下参数且返回结果的新函数的技术

function currying(fn) {
	let args = []
	return function temp(...newArgs) {
		if (newArgs.length) {
			args = [...args, ...newArgs]
			return temp
		} else {
			let val = fn.apply(this, args)
			args = []
			return val
		}
	}
}
function add(...args) {
	return args.reduce((a, b) => (a + b))
}
let addCurry = curring(add)
console.log(addCurry(1)(2)(3)(4, 5)())
console.log(addCurry(1)(2)(3, 4, 5)())
console.log(addCurry(1)(2, 3, 4, 5)())

介绍防抖节流原理、区别以及应用,并用JavaScript进行实现

防抖

  • 原理:在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时
  • 适用场景
    • 按钮提交:防止多次提交按钮,只执行最后一次的提交
    • 搜索框联想:防止联想发送多次请求,只发送最后一次输入
function debounce(func, wait, immediate = false) {
	let timeout = ''
	let result = ''
	return function() {
		const context = this
		const args = arguments
		timeout && clearTimeout(timeout)
		if (immediate) { // 立即执行 func
			const callNow = !timeout
			timeout = setTimeout(function() {
				timeout = null
			}, wait)
			if (callNow) result = func.apply(context, args)
		} else {
			timeout = setTimeout(function() {
				func.apply(context, args)
			}, wait)
		}
		return result
	}
}

节流

  • 原理:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效
  • 适用场景
    • 拖拽:固定时间内只执行一次,防止超高频次触发位置变动
    • 缩放:监控浏览器 resize
// 使用时间戳实现
function throttle(func, wait) {
	let context = ''
	let args = ''
	let previous = 0
	return function() {
		let now = +(new Date())
		context = this
		args = arguments
		if (now - previous > wait) {
			func.apply(context, args)
			previous = now
		}
	}
}
// 使用定时器实现
function throttle(func, wait) {
	let timeout = ''
	return function() {
		const context = this
		const args = arguments
		if (!timeout) {
			timeout = setTimeout(function() {
				timeout = null
				func.apply(context, args)
			}, wait)
		}
	}
}

闭包

闭包可以实现在一个内层函数中访问到其外层函数的作用域,是由函数以及声明该函数的语法环境组合而成的,该环境包含了这个闭包创建时作用域内的任何局部变量。

function makeFunc() {
	var name = 'Mozilla'
	function displayName() {
		alert(name)
	}
	return displayName
}
var myFunc = makeFunc()
myFunc()

在本例子中,myFunc 是执行 makeFunc 时创建的 displayName 函数实例的引用,displayName 的实例维持了一个对它的语法环境(变量 name 存在于其中)的引用。因此,当 myFunc 被调用时,变量 name 仍然可用,其值 Mozilla 就被传递到 alert 中。

function makeAdder(x) {
	return function(y) {
		return x + y
	}
}
var add5 = makeAdder(5)
var add10 = makeAdder(10)
console.log(add5(2)) // 7
console.log(add10(2)) // 12

从本质上讲,makeAdder 是一个函数工厂,它创建了将指定的值和它的参数相加求和的函数。在上面的示例中,使用函数工厂创建了两个新函数:一个将其参数和 5 求和,另一个和 10 求和。
add5 和 add10 都是闭包,它们共享相同的函数定义,但是保存了不同的语法环境。在 add5 的环境中,x 为 5,而在 add10 中,x 则为 10。

闭包模拟私有方法

var makeCounter = function() {
	var privateCounter = 0
	function changeBy(val) {
		privateCounter += val
	}
	return {
		increment: function() {
			changeBy(1)
		},
		decrement: function() {
			changeBy(-1)
		},
		value: function() {
			return privateCounter
		}
	}
}
var counter1 = makeCounter()
var counter2 = makeCounter()
console.log(counter1.value()) // 0
counter1.increment()
counter1.increment()
console.log(counter1.value()) // 2
counter1.decrement()
console.log(counter1.value()) // 1
conosle.log(counter2.value()) // 0

常见错误使用

function showHelp(help) {
	document.getElementById('help').innerHTML = help
}
function setupHelp() {
	var helpText = [
		{ id: 'email', help: 'Your e-mail address' },
		{ id: 'name', help: 'Your full name' },
		{ id: 'age', help: 'Your age' }
	]
	for (var i = 0; i < helpText.length; i++) {
		var item = helpText[i]
		document.getElementById(item.id).onfocus = function() {
			showHelp(item.help)
		}
	}
}
setupHelp()

以上代码运行,无论焦点在哪个 input上,显示的都是关于年龄的信息。原因是赋值给 onfocus 的是闭包,这些闭包是由其函数定义和在 setupHelp 作用域中捕获的环境所组成的。
这三个闭包在循环中被创建,但它们共享了同一个词法作用域,在这个作用域中存在一个变量 item。变量 item 使用 var 进行声明,由于变量提升,所以具有函数作用域。当 onfocus 的回调执行时,由于循环在事件触发之前早已执行完毕,变量对象 item 已经指向了 helpText 的最后一项。

优点

  • 可以在内层函数中访问到外层函数作用域中的变量,且访问到的变量长期驻扎在内存中,可供以后使用
  • 避免变量污染全局
  • 把变量存到独立的作用域,作为私有成员存在

缺点

  • 对内存消耗有负面影响,因为内部函数保存了对外部变量的引用,导致无法被垃圾回收,增大内存使用量,所以使用不当会造成内存泄漏
  • 对处理速度具有负面影响,闭包的层级决定了引用的外部变量在查找时经过的作用域链长度
  • 可能获取到意外的值
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值