# 数据类型
# undefined与void 0
undefined代表的是未定义,undefined是由null派生来的,因此将它们定义为相等,但不全等。增加这个特殊值的目的就是为了正式明确空对象指针(null)和未初始化变量的区别。
undefined 在JavaScript中不是一个保留字,这意味着可以使用undefined作为一个变量来使用赋值。
undefined 在ES5中已经是全局对象的一个只读属性了,它不能被重写,但是在局部作用域中还是可以被重写的。它会影响对undefined值的判断,我们可以用void 0
作为undefined的替代,因为void运算符能对给定表达式进行求值,然后返回undefined。也就是说void后面可以随便给定表达式,但 void 0 是表达式中最短的,且能节省字节。
# null是Object吗?
null 代表的含义是空对象,且typeof null==="object",但这只是一个JS历史遗留的bug。
在JS的最初版本中使用的是32位系统,为了性能考虑使用低位存储变量的类型信息,其中 000 开头代表是对象,然而 null 表示为全零,所以被错误的判定为object。虽然现在的内部类型判断代码已经改变,但是这个bug一直存在。
# 关于NaN
typeof NaN === "number"
isNaN和Number.isNaN函数的区别
- 函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。
- 函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。
# typeof与instanceof
typeof 对于原始类型来说,除了null会显示成object,其他都可正常显示;对于对象来说,除了函数都会显示object。所以说typeof并不能准确判断变量到底是什么类型的。
如果想判断一个对象的正确类型,可以考虑使用 instanceof,其内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。自定义实现:
function myInstanceof(left, right) {
// 原始类型直接返回false
if(left === null || (typeof left!= "object" && typeof left!= "function")) return false;
// 获取对象的原型
let proto = Object.getPrototypeOf(left);
// 获取构造函数的 prototype 对象
let prototype = right.prototype;
// 判断构造函数的 prototype 对象是否在对象的原型链上
while (true) {
if (!proto) return false;
if (proto === prototype) return true;
// 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
proto = Object.getPrototypeOf(proto);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
对于原始类型来说,想直接通过 instanceof 来判断类型是不行的,但改一下instanceof还是可以判断原始类型的:
class PrimitiveString {
static [Symbol.hasInstance](x) {
return typeof x === 'string'
}
}
console.log('hello world' instanceof PrimitiveString) // true
2
3
4
5
6
Symbol.hasInstance
是一个能让我们自定义 instanceof 行为的东西,以上代码等同于 typeof 'hello world' === 'string'
,所以结果自然是 true 了。
这其实也侧面反映了一个问题, instanceof 也不是百分之百可信的。
# 为什么会有BigInt的提案?
JavaScript中Number.MAX_SAFE_INTEGER表示最大安全数字,计算结果是9007199254740991(2^53-1),即在这个数范围内不会出现精度丢失(小数除外)。但是⼀旦超过这个范围,js就会出现计算不准确的情况,这在大数计算的时候不得不依靠⼀些第三方库进行解决,因此官方提出了BigInt来解决此问题。
# 类型转换
转Boolean:在条件判断时,除了 undefined
, null
, false
, NaN
, ''
, 0
, -0
,其他所有值都转为 true
,包括所有对象。
**"=="**运算符:
当两个操作数的类型不相同时
- 如果一个值是null,另一个值是undefined,则它们相等
- 如果一个值是数字,另一个值是字符串,先将字符串转换为数学,然后使用转换后的值进行比较。
- 如果其中一个值是true,则将其转换为1再进行比较。如果其中的一个值是false,则将其转换为0再进行比较。
- 如果一个值是对象,另一个值是数字或字符串,则将对象转换为原始值,再进行比较。
这里有三道题:
[]==![] // true
// 首先先执行的是![],它会得到false,然后[]==false,返回true。
[]==[] // false
{}=={} // false
// 类型一致,它们是引用类型,地址是不一样的,所以为false!
2
3
4
5
6
包装类型:在 JavaScript 中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时 JavaScript 会在后台隐式地将基本类型的值转换为对象,再访问其属性。
隐式类型转换:ToPrimitive(需要转换的对象,期望的结果类型)
方法是 JavaScript 中每个值隐含的自带的方法,用来将值 (无论是基本类型值还是对象)转换为基本类型值。如果值为基本类型,则直接返回值本身;如果值为对象,就进行对象转原始类型。
JavaScript 中的隐式类型转换主要发生在+、-、*、/
以及==、>、<
这些运算符之间。而这些运算符只能操作基本类型值,所以在进行这些运算前的第一步就是将两边的值用ToPrimitive
转换成基本类型,再进行操作。
对象转原始类型:
对象在转换类型的时候,会调用内置的 [[ToPrimitive]]
函数,对于该函数来说,算法逻辑一般来说如下:
- 如果已经是原始类型了,那就不需要转换了
- 如果需要转字符串类型就先调用
x.toString()
,结果不是基础类型的话再调用valueOf
,转换为基础类型的话就返回转换的值。 - 如果需要转数值类型的话就先调用
valueOf
,结果不是基础类型的话再调用toString
。 - 如果都没有返回原始类型,就会抛 TypeError 异常。
- 如果对象为 Date 对象,则
type
默认为string
;其他情况下,type
默认为number
。
当然可以重写 Symbol.toPrimitive
,该方法在转原始类型时调用优先级最高。
let a = {
valueOf() {
return 0
},
toString() {
return '1'
},
[Symbol.toPrimitive]() {
return 2
}
}
1 + a // => 3
2
3
4
5
6
7
8
9
10
11
12
加减乘除运算符:
加法运算符不同于其他几个运算符,它有以下几个特点:
- 运算中其中一方为字符串,那么就会把另一方也转换为字符串
- 如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"
2
3
如果你对于答案有疑问的话,请看解析:
- 对于第一行代码来说,触发特点一,所以将数字
1
转换为字符串,得到结果'11'
- 对于第二行代码来说,触发特点二,所以将
true
转为数字1
- 对于第三行代码来说,触发特点二,所以将数组通过
toString
转为字符串1,2,3
,得到结果41,2,3
另外对于加法还需要注意这个表达式 'a' + + 'b'
'a' + + 'b' // -> "aNaN"
因为 + 'b'
等于 NaN
,所以结果为 "aNaN"
。
对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字
4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN
2
3
比较运算符:
- 如果是对象,就通过
toPrimitive
转换对象 - 如果是字符串,就通过
unicode
字符索引来比较
let a = {
valueOf() {
return 0
},
toString() {
return '1'
}
}
a > -1 // true
2
3
4
5
6
7
8
9
在以上代码中,因为 a
是对象,所以会通过 valueOf
转换为原始类型再比较值。
# 判断数组
var arr = []
console.log(arr instanceof Array) // true
console.log(Array.prototype.isPrototypeOf(arr)) // true
console.log(arr.constructor === Array) // true
console.log(Object.prototype.toString.call(arr) === "[object Array]") // true
console.log(Array.isArray(arr)) // true
console.log(Object.getPrototypeOf(arr) === Array.prototype) // true
2
3
4
5
6
7
# 数组去重
# for in、for of、forEach、map
- map:数组方法,不改变原数组,返回新数组,可以使用break中断,可以return到外层函数
- forEach:数组方法,不可以使用break中断,不可以return到外层函数
- for-in:用于遍历数组和对象,遍历对象键值(key),或者数组下标,不推荐循环一个数组;如果要迭代的变量是null或undefined,则不执行循环体。
- for-of:遍历Arrays(数组),Strings(字符串),Maps(映射),Sets(集合)等可迭代的数据结构,按照可迭代对象的 next() 方法产生值的顺序迭代元素。在 ES6 中引入的 for-of 循环,以替代 for-in 和 forEach() ,并支持新的迭代协议。
for-in和for-of比较:
- 对于数组的遍历,for-in循环出的是key,for-of循环出的是value。for-in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for-of 只返回数组的下标对应的属性值。
- 对于对象的遍历,for-in 和 Object.keys(myObject) 都用于枚举对象属性,但for-in遍历是会包括原型方法和属性(性能非常差不推荐使用),Object.keys()不包括。
# 0.1+0.2!=0.3?
# 原型和原型链
每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层以此类推,这种关系常被称为原型链,原型链的尽头是Object.prototype
。
prototype:每个函数都有一个prototype属性,这个属性指向的是对象原型。
__proto__
:__proto__
是一个非 es 标准的属性,它对应的是 es 标准中的 [[prototype]]
,因为 [[prototype]] 是一个内部属性,无法直接访问,所以 es6 中提供了 Object.getPrototypeOf/Object.setPrototypeOf
来读取、操作 [[prototype]],所以说 __proto__
实际是 getter/setter
,即
obj.__proto__
===>
get __proto__ = function() { return Object.getPrototypeOf(this) }
set __proto__ = function(newPrototype) { return Object. setPrototypeOf(this, newPrototype) }
2
3
4
constructor:每一个原型都有一个constructor属性指向关联的构造函数。
原型链的指向:
function Person(name) {
this.name = name;
}
var p = new Person('hello');
/**
* 正常的原型链都会终止于 Object 的原型对象
* Object 原型的原型是 null
* 构造函数 Person
* 构造函数的原型对象 Person.prototype
* 实例的原型对象 p.__proto__
* 原型对象指回构造函数 Person.prototype.constructor===Person
* Person.prototype===p.__proto__
*/
p.__proto__ // Person.prototype
Person.prototype.__proto__ // Object.prototype
p.__proto__.__proto__ //Object.prototype
p.__proto__.constructor.prototype.__proto__ // Object.prototype
Person.prototype.constructor.prototype.__proto__ // Object.prototype
p.__proto__.constructor // Person
Person.prototype.constructor // Person
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
需要注意的地方:
在传统的OOP中,首先定义"类",再创建对象实例时,类中定义的所有属性和方法都被复制到实例中。而JS中的对象是通过引用来传递的,创建的每个新对象实体中并没有一份属于自己的原型副本,而是在对象实例和它的构造器之间建立一个链接(
__proto__属性
,是从构造函数的prototype
属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。当修改原型时,与之相关的对象也会继承这一改变。Object.prototype.__proto__ === null
跟 Object.prototype 没有原型,其实表达的是一个意思。function Person() {} var person = new Person(); console.log(person.constructor === Person); // true
1
2
3当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到 constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性,所以打印true。
# 词法作用域与动态作用域
作用域是指程序源代码中定义变量的区域。它规定了如何查找变量,即确定了当前执行代码对变量的访问权限。JavaScript采用词法作用域,也就是静态作用域。
- 静态作用域:函数的作用域在函数定义时就决定了,理解了这句话就能理解闭包
- 动态作用域:函数的作用域是在函数调用的时候才决定的
看个栗子:
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();// 结果是 ???
2
3
4
5
6
7
8
9
假设JavaScript采用静态作用域,让我们分析下执行过程:
执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。
假设JavaScript采用动态作用域,让我们分析下执行过程:
执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。
因为JavaScript采用的是静态作用域,所以这个例子的结果是 1。
再看个栗子:
// 1
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
// 2
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
两段代码都会打印:local scope
,因为JavaScript采用的是词法作用域,函数的作用域基于函数创建的位置。
而引用《JavaScript权威指南》的回答就是:JavaScript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数 f() 定义在这个作用域链里,其中的变量 scope 一定是局部变量,不管何时何地执行函数 f(),这种绑定在执行 f() 时依然有效。
虽然两段代码执行的结果一样,但不同之处就在于执行上下文栈的变化不一样。第一段代码执行上下文栈有两层;而第二段代码栈只有一层(这里没有考虑全局上下文)。
# 执行上下文栈
JS引擎并非一行一行地分析和执行程序,而是一段一段地分析执行。当执行一段代码的时候,会进行一个“准备工作”,比如变量提升,函数提升等。那么这一段一段的”段“怎么划分?JS引擎遇到一段怎么样的代码才会做“准备工作”呢?
可执行代码:JS的可执行代码就四种:全局代码、函数代码、eval代码、module代码。例如,当执行到一个函数时,就会进行准备工作,而这里的准备工作其实就是执行上下文
。我们可能创建很多的函数,所以JS引擎创建了**执行上下文栈(ECS)**来管理执行上下文。
全局上下文是最外层的上下文。当代码执行流进入函数时,函数的上下文就被推到一个上下文栈上,在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。JavaScript是单线程的,JavaScript程序的执行流就是通过这个上下文栈进行控制的。
执行上下文分为创建阶段和执行阶段,以函数执行上下文为例:
- 当调用函数时,在还没执行里面代码前,即创建阶段,执行上下文会创建变量对象,作用域链和决定this值
- 进入执行阶段,会顺序执行函数中每一行代码,进行变量赋值、函数引用等动作
# 变量对象
不同的执行上下文中的变量对象稍有不同,主要介绍一下全局上下文和函数上下文中的变量对象。
全局上下文:
全局上下文的变量对象就是全局对象
。- 全局对象是由Object构造函数实例化的一个对象。在客户端JS中,全局对象就是Window对象。
- 全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性,比如Math、String、Date等。
- 在顶层JS代码中,可以用关键字 this 引用全局对象。
- 因为全局对象是作用域链的头,所以在顶层JS代码中声明的所有变量都将成为全局对象的属性。
函数上下文:
- 在函数上下文中,我们用
活动对象(AO)
来表示变量对象。 - 活动对象和变量对象其实是一个对象,只是处于执行上下文的不同生命周期。变量对象是规范上或者说是引擎实现上的,不可在JS环境中访问,只有到进入一个执行上下文中(
执行阶段
),这个执行上下文的变量对象才会被激活,所以叫活动对象。而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。
也就是说,在执行上下文的创建阶段会创建变量对象;在执行阶段,变量对象会被激活为活动对象。
变量对象包括:
- 函数的所有形参(如果是函数上下文):
- 由名称和对应值组成的一个变量对象的属性被创建
- 没有实参,属性值设为undefined
- 函数声明
- 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
- 如果变量对象已经存在相同名称的属性,则完全替换这个属性
- 变量声明
- 由名称和对应值(undefined)组成一个变量对象的属性被创建
- 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性(不会覆盖)
- let/const声明的变量也会提升,但是不会给它赋值undefined,也就是说虽然变量存在,但是没有赋值,还是不能访问,这就是常说的暂时性死区
举个栗子:
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1);
2
3
4
5
6
7
进入执行上下文后,分析得到的AO是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
2
3
4
5
6
7
8
9
10
进入执行阶段,根据代码,修改变量对象的值,执行完后的AO是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
2
3
4
5
6
7
8
9
10
话不多说,看题🧐:
function foo() {
console.log(a);
a = 1;
}
foo(); // ???
function bar() {
a = 1;
console.log(a);
}
bar(); // ???
2
3
4
5
6
7
8
9
10
11
12
第一段会报错:Uncaught ReferenceError: a is not defined
;第二段会打印:1
。
第一段执行时,因为函数foo中的"a"没有通过 var 或其他关键字声明,所以不会被存放在AO中。去全局中也没有找到,所以报错。
第二段执行时,全局对象已经被赋予了"a"属性,这时候就可以从全局中找到"a",打印1。
# 作用域链
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链
。
函数创建:
前面说到,函数的作用域在函数定义的时候就决定了。这是因为函数有一个内部属性[[scope]],当函数创建的时候,就会保存所有父变量对象到其中,可以理解为[[scope]]就是所有父变量对象的层级链,但并不代表完整的作用域链!
举个栗子:
function foo() {
function bar() {
...
}
}
// 函数创建时,各自的[[scope]]为:
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
];
2
3
4
5
6
7
8
9
10
11
12
13
14
15
函数激活:
当函数激活时,进入函数上下文,创建VO/AO后,就会将活动对象添加到作用域链的前端。假设此时执行上下文的作用域链命名为Scope:
Scope = [AO].concat([[Scope]]);
至此,作用域链创建完毕。
来捋一下整个过程:
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
2
3
4
5
6
- checkscope 函数被创建,保存父级变量对象到内部属性[[scope]]
- 调用 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
- checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
- 第二步:用 arguments 创建变量对象,随后初始化变量对象,加入形参、函数声明、变量声明
- 第三步:将变量对象压入 checkscope 作用域链顶端
- 准备工作做完,开始执行函数,这里变量对象被激活为活动对象,随着函数的执行,修改 AO 的属性值
- 查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
# this
先看一道题:
function foo() {
console.log(this.a)
}
var a = 1
foo(); // 1
const obj = {
a: 2,
foo: foo
}
obj.foo() // 2
const c = new foo() // 3
2
3
4
5
6
7
8
9
10
11
- 直接调用foo:不管foo函数被放在了地方,this 一定是 window。
- 作为对象属性obj.foo():谁调用了函数,this就指向谁,所以在这里foo函数的this就是obj对象。
- 使用new调用:this被绑定在了实例 c 上面,使用bind无法改变其指向,但call、apply可以。
这几种场景是最常见的,接下来看看箭头函数中的this:
function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
2
3
4
5
6
7
8
箭头函数其实是没有this的,箭头函数中的this永远指向上级作用域的this。在这个例子中,箭头函数的this指向函数 a 的this,而函数 a 的this就是window,所以此时的this就是window。另外对箭头函数使用bind、call和apply函数是无效的,即箭头函数的this一旦被绑定,就不会被任何方式改变
。
当以上多个规则同时出现时,根据优先级最高的来决定 this 最终的指向:
使用new调用 > bind这类函数 > 作为对象属性obj.foo()调用 > 直接调用foo
# 闭包
闭包是指那些能够访问自由变量的函数。自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量(父级变量)。
从理论上讲,所有函数都是闭包,因为它们在创建的时候就将上层上下文的数据保存起来了。
从实践角度讲,满足以下条件的函数才属于闭包:
- 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
- 在代码中引用了自由变量
简单来说就是,闭包就是在定义这个函数的词法作用域外还能被调用的函数。闭包在函数创建时创建。
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope();
foo();
2
3
4
5
6
7
8
9
10
11
在这个栗子中,checkscope 函数上下文出栈后,foo 函数上下文入栈执行,但依旧可以读取到checkscope作用域下scope的值。这是因为foo执行上下文维护了一个作用域链,即使checkscope的上下文被销毁了,foo的变量对象依旧在内存中,foo函数依然可以通过其作用域链找到scope,从而实现了闭包这个概念。
到做题的时候了:
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
2
3
4
5
不出意外,将打印出5个6,因为 setTimeout 是异步函数,所以会先把循环全部执行完毕,这个时候i就是6了。有下面3种解决办法:
使用闭包
for (var i = 1; i <= 5; i++) { (function(j) { setTimeout(function timer() { console.log(j) }, j * 1000) })(i) }
1
2
3
4
5
6
7为什么将i传进函数就行了呢?因为j是匿名函数的形参,会被包含进执行上下文创建的变量对象中,而j中保存了此时i的值,所以setTimeout里面代码执行的时候能打印出预期答案。
使用setTimeout第三个参数
for (var i = 1; i <= 5; i++) { setTimeout( function timer(j) { console.log(j) }, i * 1000, i )// 第三个参数会被当成timer函数的参数传入 }
1
2
3
4
5
6
7使用let定义i
闭包的应用场景:
- 使用闭包来模拟私有方法
如果不是某些特定任务需要使用闭包,在其他函数中创建闭包是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
# call、apply、bind
这三个方法都是Function.prototype下的方法,均用于改变函数运行时上下文(this的指向)。
call和apply作用都是相同的,只是传参方式不同,除了第一个参数外,call可以接收一个参数列表,apply只接收一个参数数组。
bind和其他两个方法作用一致,但bind会返回一个函数。
# 实现call
- 参数:第一个是this绑定的对象(不填默认为window);后面的其余参数是传入调用call的函数执行的参数
- 实现思路就是将调用call的函数加入到对象的属性中,这样就改变了this指向,为了不影响对象的值,执行完后再删除这个加上去的属性
Function.prototype.myCall = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
console.error("type error");
}
// 获取参数
let args = [...arguments].slice(1),
result = null;
// 判断 context 是否传入,如果未传入则设置为 window
context = context || window;
// 将调用函数设为对象的方法
context.fn = this;
// 调用函数
result = context.fn(...args);// 这个fn就是调用call的函数bar,如果bar有返回值,则存入result中
// 将属性删除
delete context.fn;
return result;
};
// 测试
var value = 2;
var obj = {
value: 1
}
function bar(name, age) {
console.log(this.value);
return {
value: this.value,
name: name,
age: age
}
}
bar.myCall(null); // 2
console.log(bar.myCall(obj, 'kevin', 18));
// 1
// Object {
// value: 1,
// name: 'kevin',
// age: 18
// }
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
# 实现apply
- apply接收两个参数:第一个参数指定this绑定的对象;第二个参数是个数组
//思路一致
Function.prototype.myApply=function(context, args = []){
if(typeof this !=='function'){
throw new TypeError('error');
}
context=context||window;
context.fn=this;
let result;
// 参数处理有区别,第二个参数是一个数组
result=context.fn(...args);
delete context.fn;
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
# 实现bind
- bind内部要返回一个函数,而fun.bind(obj,arg1,...)返回一个原函数的拷贝,并拥有指定的this值和初始参数。
- bind返回的函数不会马上执行,且还可以再传入参数!
Function.prototype.myBind=function(context){
if (typeof this !== 'function') {
throw new TypeError('error');
}
const fn=this;// this是调用bind的函数
const args=[...arguments].slice(1);
return function Fn(){
// 如果使用 new 来调用函数,则不改this的执行,因为使用new的方式,任何方法都不能改变其this指向
return fn.apply(
this instanceof Fn? this : context,
args.concat(...arguments)// 这里的arguments是指传入 Fn 的参数,将之前传的参数合并
);
}
}
// 测试
function test() {...}
let boundTest = test.myBind({name: 'doubao'}, 1, 2, 3);
let boundRes = boundTest(4);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 应用
使用Object.prototype.toString.call()来判断类型
因为
Object.prototype.toString() 方法会返回对象的类型字符串
,输出"[object Object]"
。其中第二个Object
是传入参数的构造函数。所以使用call
就可以指定任意的值和结合toString
将组成的构造函数类型返回来判断类型。同样道理换成apply/bind
同样也可以判断。如Object.prototype.toString.call([])。使用call()实现将类数组转化成数组
let array = [12, 23, 45, 65, 32] function fn(array){ var args = [].slice.call(arguments) // 或使用Array.prototype.slice() return args[0] } fn(array) // [12, 23, 45, 65, 32]
1
2
3
4
5
6
7上面利用 call 改变了 slice 的 this 指向
arguments
来遍历输出(slice的this原本指向[],转变后类似于[12, 23, 45, 65, 32].slice()
的效果)。
# new与构造函数
new操作符的执行过程:
- 创建了一个新的空对象
- 设置原型,将对象的原型设置为函数的prototype对象(实例与原型的关联)
- 让函数的this指向这个对象,执行构造函数的代码(为这个新对象添加属性)
- new操作符执行完后会返回一个对象
- 判断构造函数是否有返回值。如果返回值为基本类型或null,返回new中新创建的对象;如果是引用类型,就返回这个引用类型的对象(二选一)
所以在使用new构造函数时,尽量不要写返回值,如果返回值为对象,那new就没有原本的作用了。
new的模拟实现:
因为new是关键字,无法像bind函数一样直接覆盖,所以模拟实现时使用函数,命名为newObj。
function newObj() {
let Constructor = [].shift.call(arguments);// 拿到构造函数
let args = [...arguments].slice(1); // 拿到传给构造函数的参数
let obj = Object.create(Constructor.prototype);//create中的函数是要创建对象的原型
let ret = Constructor.apply(obj, args);// apply会绑定this,并执行函数
return ret instanceof Object ? ret : obj;
}
2
3
4
5
6
7
# 类数组对象与arguments
类数组对象就是拥有一个length属性和若干索引属性的对象
。类数组对象可以像数组一样读写、获取长度及遍历,但是类数组对象不能调用数组的方法。常见的类数组对象有arguments、DOM方法的返回结果。
使用call和apply间接调用数组方法:
var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }
Array.prototype.join.call(arrayLike, '&'); // name&age&sex
Array.prototype.slice.call(arrayLike, 0); // ["name", "age", "sex"]
// slice可以做到类数组转数组
Array.prototype.map.call(arrayLike, function(item){
return item.toUpperCase();
});
// ["NAME", "AGE", "SEX"]
2
3
4
5
6
7
8
类数组转数组的4种方法:
var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }
// 1. slice
Array.prototype.slice.call(arrayLike); // ["name", "age", "sex"]
// 2. splice
Array.prototype.splice.call(arrayLike, 0); // ["name", "age", "sex"]
// 3. ES6 Array.from
Array.from(arrayLike); // ["name", "age", "sex"]
// 4. concat
Array.prototype.concat.apply([], arrayLike)
2
3
4
5
6
7
8
9
# Arguments对象
Arguments 对象只定义在函数体中,包括了函数的参数和其他属性。在函数体中,arguments 指代该函数的 Arguments 对象。
举个栗子:
function foo(name, age, sex) {
console.log(arguments);
}
foo('name', 'age', 'sex')
2
3
4
打印结果:
length属性:表示函数实参
的个数,而不是形参,传了多少就是多少。
callee属性:通过它可以调用函数自身。可以解决闭包和递归函数中引用丢失的情况。
func.caller表示的是调用当前函数的函数,全局下返回null。callee和caller在严格模式下都会抛出错误。
// 闭包
var data = [];
for (var i = 0; i < 3; i++) {
(data[i] = function () {
console.log(arguments.callee.i)
}).i = i;
}
data[0]();// 0
data[1]();// 1
data[2]();// 2
// 递归
function factorial(num) {
if(num <= 1) return 1
return num * arguments.callee(num - 1);
}
let fn = factorial;
factorial = null;
console.log(fn(4)); // 24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 防抖和节流
在前段开发中会遇到一些频繁的事件触发,比如:
- window的resize、scroll
- mousedown、mousemove
- keyup、keydown
- ...
如果是复杂的回调函数或ajax被频繁触发呢?假设1秒触发了60次,每个回调就必须在 1000/60=16.67ms内完成,否则就会有卡顿。为了解决这个问题,一般有两种解决方案:
- debounce 防抖
- throttle 节流
防抖是虽然事件持续触发,但只有等事件停止触发后 n 秒才执行函数,节流是持续触发的时候,每 n 秒执行一次函数。
# 防抖
特点:
- 对于在事件被触发n秒后在执行回调,即
延迟n秒执行
- 如果在这n秒内再触发事件,就
重新开始计时
- 如果传入了第三个参数,事件就会立即执行,
在n秒后恢复立即执行
,特点2也适用
function debounce(func, wait, immediate) {// 第三个参数表示是否立即执行第一次
var timeout, result;
var debounced = function () {
var self = this;
// 这里的this指向的是下面例子中的container,让要调用的函数的this也指向container,不然它会指向全局
var args = arguments;
if (timeout) clearTimeout(timeout); // 如果有定时器就清除定时器,因为后面需要重新计时
// 1. 立即执行的情况
if (immediate) {
var callNow = !timeout;
// 重新计时:n秒后置定时器为null,这样又可以触发执行了
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) result = func.apply(self, args)
// 如果定时器为空,说明已经过了n秒,立即执行且重新计时
// 如果不为空,说明还没有到n秒时间,重新计时
}
// 2. 延迟执行的情况
else {
timeout = setTimeout(function(){
func.apply(self, args) // func()的进化版,因为考虑func可能有返回值
}, wait);
}
return result;// 返回func函数返回值
};
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
return debounced;
}
// 使用:输出输入框的值
var container = document.getElementById('container');
function get() {
console.log(this.value);
}
var db = debounce(get, 1000, true);
// 输入框每次输入都会执行返回函数debounced,因为形成了闭包,所以可以保留上次定时器
container.oninput = db;
document.getElementById("btn").addEventListener('click', function(){
db.cancel();
})
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
html部分:
<input type="text" id="container" />
<button id="btn">取消debounce</button>
2
需要注意的几个问题:
为什么要用apply绑定this? 假设func中有用到this,比如通过this.value来获取输入框的值,那么如果不使用apply的话,this会指向全局,而这里要求func指向调用这个函数的对象。
immediate立即执行是什么意思? 不希望是在事件被触发后n秒执行,而是只要被触发就立即执行,但是执行后,必须等到n秒后才能再次触发执行,如果n秒内再次触发,则不执行且重新计时。
clearTimeout(timer)和timer=null有什么区别?
前者只是清除了定时器,而不是清除timer为空值;而后者是将timer置为空,而没有清除定时器。
# 节流
特点:
- 如果持续触发事件,每隔一段时间,只执行一次事件。
- 有两种情况:首次事件触发是否立即执行;停止触发后是否要再执行一次
- 关于节流的实现,有两种主流方式:
- 时间戳:当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。
- 设置定时器:当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。
/*
第三个参数可选择想要的效果:
1. leading:false 表示禁用第一次执行
2. trailing: false 表示禁用停止触发的回调
*/
function throttle(func, wait, options) {
var timeout, context, args, result;
var previous = 0;// 时间戳
if (!options) options = {};// 如果没有传这个参数就设为空对象
var later = function() {
previous = options.leading === false ? 0 : new Date().getTime();
timeout = null;
func.apply(context, args);
if (!timeout) context = args = null;
};
var throttled = function() {
var now = new Date().getTime();// 获取当前时间戳
if (!previous && options.leading === false) previous = now; //只在previous为0,且需要首次执行的时候才执行
//下次触发 func 剩余的时间(每次触发都要计算)
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果没有剩余的时间了或者你改了系统时间
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
};
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = null;
}
return throttled;
}
// 使用,默认第三个选项都为true
container.oninput = throttle(get, 1000);
container.oninput = throttle(get, 1000, {
leading: false
});
container.oninput = throttle(get, 1000, {
trailing: false
});
/*
leading:false 和 trailing: false 不能同时设置。
如果同时设置的话,比如当你将鼠标移出的时候,因为 trailing 设置为 false,停止触发的时候不会设置定时器,所以只要再过了设置的时间,再移入的话,就会立刻执行,就违反了 leading: false,bug 就出来了。
*/
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
58
59
# 深拷贝与浅拷贝
# 浅拷贝
特点:如果数组元素是基本类型,就会拷贝一份,互不影响。而如果是对象或者数组,就会只拷贝对象和数组的引用,这样我们无论在新旧数组进行了修改,两者都会发生变化。
// 对象的浅拷贝
let a = {
age: 1
}
// 1. Object.assign 拷贝所有属性值到新对象中,如果属性值是对象的话,拷贝地址
let b = Object.assign({}, a)
// 2. 展开运算符
let b = {...a}
// 数组的浅拷贝
var arr = ['old', 1, true, null, undefined];
// 1. concat
var new_arr = arr.concat();
// 2. slice
var new_arr = arr.slice();
new_arr[0] = 'new';
console.log(arr) // ["old", 1, true, null, undefined]
console.log(new_arr) // ["new", 1, true, null, undefined]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
实现:
var shallowCopy = function(obj) {
// 只拷贝对象
if (typeof obj !== 'object') return;
// 根据obj的类型判断是新建一个数组还是对象
var newObj = obj instanceof Array ? [] : {};
// 遍历obj,并且判断是obj的属性才拷贝
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
}
return newObj;
}
2
3
4
5
6
7
8
9
10
11
12
13
# 深拷贝
特点:完全的拷贝一个对象,即使嵌套了对象,两者也相互分离,修改一个对象的属性,也不会影响另一个。
这个问题通常可以通过JSON.parse(JSON.stringify(object))
来解决,但也有局限性:
- 会忽略 undefined 和 symbol
- 不能序列化函数
- 不能解决循环引用的问题
loader中可以使用deepclone解决。
实现:
var deepCopy = function(obj) {
if (typeof obj !== 'object') return;
var newObj = obj instanceof Array ? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
}
}
return newObj;
}
2
3
4
5
6
7
8
9
10