数据类型

内置类型

  • JavaScript 中分为七种内置类型,七种内置类型又分为两大类:基本类型和对象(Object)。
  • 基本类型有七种:nullundefinedbooleannumberstringsymbolbigint
  • 其中JavaScript的数字类型是浮点类型,没有整形。并且浮点类型基于 IEEE 754 标准实现,在使用中会遇到某些Bug。 NaN 也属于 number 类型,并且 NaN不等于自身。
  • 对于基本类型来说,如果使用字面量的方式,那么这个变量只是个字面量,只有在必要的时候才会转换为对应的类型。

引用数据类型:

  • 对象 Object(包含普通对象-Object,数组对象-Array,正则对象-RepExp,日期对象-Date,数学函数-Math,函数对象-Function

    1
    2
    let a = 111 // 这只是字面量,不是 number 类型
    a.toString() // 使用时候才会转换为对象类型

    对象(Object)是引用类型,在使用过程中会遇到浅拷贝深拷贝的问题。

    1
    2
    3
    4
    let a = { name: 'FE' }
    let b = a
    b.name = 'EF'
    console.log(a.name) // EF

    说出下面运行的结果,解释原因。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function test(person) {
    person.age = 26
    person = {
    name: 'hzj',
    age: 18
    }
    return person
    }
    const p1 = {
    name: 'fyq',
    age: 19
    }
    const p2 = test(p1)
    console.log(p1) // -> ? -> p1:{name: “fyq”, age: 26}
    console.log(p2) // -> ? -> p2:{name: “hzj”, age: 18}

    原因: 在函数传参的时候传递的是对象在堆中的内存地址值,test函数中的实参person是p1对象的内存地址,通过调用person.age = 26确实改变了p1的值,但随后person变成了另一块内存空间的地址,并且在最后将这另外一份内存空间的地址返回,赋给了p2。

null和undefined区别

Undefined类型只有一个值,即undefined。当声明的变量还未被初始化时,变量的默认值为undefined。用法

  • 变量被声明了,但没有赋值时,就等于 undefined
  • 调用函数时,应该提供的参数没有提供,该参数等于 undefined
  • 对象没有赋值的属性,该属性的值为 undefined
  • 函数没有返回值时,默认返回 undefined

Null类型也只有一个值,即null。null用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象。用法

  • 作为函数的参数,表示该函数的参数不是对象。
  • 作为对象原型链的终点

null是对象吗?为什么?

null不是对象。
解释: 虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象然而 null 表示为全零,所以将它错误的判断为 object 。

‘1’.toString()为什么可以调用?

其实在这个语句运行的过程中做了这样几件事情:

1
2
3
var s = new Object('1');
s.toString();
s = null;
  • 第一步: 创建Object类实例。注意为什么不是String ? 由于Symbol和BigInt的出现,对它们调用new都会报错,目前ES6规范也不建议用new来创建基本类型的包装- 类。
  • 第二步: 调用实例方法。
  • 第三步: 执行完方法立即销毁这个实例。

整个过程体现了基本包装类型的性质,而基本包装类型恰恰属于基本数据类型,包括Boolean, Number和String。

0.1+0.2为什么不等于0.3?

0.1和0.2在转换成二进制后会无限循环,由于标准位数的限制后面多余的位数会被截掉,此时就已经出现了精度的损失,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成0.30000000000000004

BigInt

什么是BigInt?

BigInt是一种新的数据类型,用于当整数值大于Number数据类型支持的范围时。这种数据类型允许我们安全地对大整数执行算术操作,表示高分辨率的时间戳,使用大整数id,等等,而不需要使用库。

为什么需要BigInt?
在JS中,所有的数字都以双精度64位浮点格式表示,那这会带来什么问题呢?

这导致JS中的Number无法精确表示非常大的整数,它会将非常大的整数四舍五入,确切地说,JS中的Number类型只能安全地表示-9007199254740991(-(253-1))和9007199254740991((253-1)),任何超出此范围的整数值都可能失去精度。

1
2
3
4
console.log(999999999999999);  //=>10000000000000000

// 同时也会有一定的安全性问题:
9007199254740992 === 9007199254740993; // → true 居然是true!

要创建BigInt,只需要在数字末尾追加n即可

1
2
console.log( 9007199254740995n );    // → 9007199254740995n	
console.log( 9007199254740995 ); // → 9007199254740996

另一种创建BigInt的方法是用BigInt()构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BigInt("9007199254740995");    // → 9007199254740995n

10n + 20n; // → 30n
10n - 20n; // → -10n
+10n; // → TypeError: Cannot convert a BigInt value to a number
-10n; // → -10n
10n * 20n; // → 200n
20n / 10n; // → 2n
23n % 10n; // → 3n
10n ** 3n; // → 1000n

const x = 10n;
++x; // → 11n
--x; // → 9n
console.log(typeof x); //"bigint"

值得警惕的点

BigInt不支持一元加号运算符, 这可能是某些程序可能依赖于 + 始终生成 Number 的不变量,或者抛出异常。另外,更改 + 的行为也会破坏 asm.js 代码。

因为隐式类型转换可能丢失信息,所以不允许在 BigintNumber 之间进行混合操作。当混合使用大整数和浮点数时,结果值可能无法由BigIntNumber精确表示。

1
10 + 10n;    // → TypeError

不能将 BigInt 传递给 Web api 和内置的 JS 函数,这些函数需要一个 Number 类型的数字。尝试这样做会报 TypeError 错误。

1
Math.max(2n, 4n, 6n);    // → TypeError

Boolean 类型与 BigInt 类型相遇时,BigInt 的处理方式与 Number 类似,换句话说,只要不是 0nBigInt 就被视为 truthy 的值。

1
2
3
4
5
6
if(0n) {//条件判断为false

}
if(3n) {//条件为true

}
  • 元素都为 BigInt 的数组可以进行 sort
  • BigInt 可以正常地进行位运算,如 |&<<>>^

类型检测

typeof

在写业务逻辑的时候,经常要用到JS数据类型的判断,面试常见的案例深浅拷贝也要用到数据类型的判断。

typeof

  • 优点:能够快速区分基本数据类型
  • 缺点:不能将 ObjectArrayNull 区分,都返回 object
1
2
3
4
5
6
7
8
console.log(typeof 2);               // number
console.log(typeof true); // boolean
console.log(typeof 'str'); // string
console.log(typeof undefined); // undefined
console.log(typeof []); // object
console.log(typeof {}); // object
console.log(typeof function(){}); // function
console.log(typeof null); // object

instenceof

  • 优点:能够区分 ArrayObjectFunction ,适合用于判断自定义的类实例对象
  • 缺点: NumberBooleanString 基本数据类型不能判断
1
2
3
4
5
6
console.log(2 instanceof Number);                    // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true

Object.prototype.toString.call()

  • 优点:精准判断数据类型
  • 缺点:写法繁琐不容易记,推荐进行封装后使用
1
2
3
4
5
6
7
8
9
10
const toString = Object.prototype.toString;

console.log(toString.call(2)); //[object Number]
console.log(toString.call(true)); //[object Boolean]
console.log(toString.call('str')); //[object String]
console.log(toString.call([])); //[object Array]
console.log(toString.call(function(){})); //[object Function]
console.log(toString.call({})); //[object Object]
console.log(toString.call(undefined)); //[object Undefined]
console.log(toString.call(null)); //[object Null]

判断是否是promise对象

1
2
3
4
5
6
function isPromise (val) {
return (
typeof val.then === 'function' &&
typeof val.catch === 'function'
)
}

typeof 于 instanceof 区别

  • typeof 对于基本类型,除了 null都可以显示正确的类型
  • typeof 对于对象,除了函数都会显示 object
  • 对于 null 来说,虽然它是基本类型,但是会显示 object,这是一个存在很久了的 Bug
  • instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 iprototype

Object.is和===的区别

Object在严格等于的基础上修复了一些特殊情况下的失误,具体来说就是+0和-0,NaN和NaN。

1
2
3
4
5
// ES6 中提供了新的 Object.is() 方法,它具有 === 的一些特点,而且更好、更精确,在一些特殊案例中表现的很好
Object.is(0 , ' '); //false
Object.is(null, undefined); //false
Object.is([1], true); //false
Object.is(NaN, NaN); //true,特别是这个

类型总结

  • typeof
    • 直接在计算机底层基于数据类型的值(二进制)进行检测
    • typeof nullobject 原因是对象存在在计算机中,都是以000开始的二进制存储,所以检测出来的结果是对象
    • typeof 普通对象/数组对象/正则对象/日期对象 都是 object
    • typeof NaN === 'number'
  • instanceof
    • 检测当前实例是否属于这个类的
    • 底层机制:只要当前类出现在实例的原型上,结果都是 true
    • 不能检测基本数据类型
  • constructor
    • 支持基本类型
    • constructor 可以随便改,也不准
  • Object.prototype.toString.call([val])
    • 返回当前实例所属类信息

判断 Target 的类型,单单用 typeof 并无法完全满足,这其实并不是 bug ,本质原因是 JS 的万物皆对象的理论。因此要真正完美判断时,我们需要区分对待:

  • 基本类型(null): 使用 String(null)
  • 基本类型(string / number / boolean / undefined) + function: - 直接使用 typeof 即可
  • 其余引用类型(Array / Date / RegExp Error): 调用 toString 后根据 [object XXX] 进行判断
1
2
3
4
5
很稳很实用的判断封装:

const type = obj => Object.prototype.toString.call(obj).match(/\s+(\w+)/)[1].toLowerCase()

const typeCheck = (o, t = null) => t ? type(o) === t : type(o)

类型转换

JS 中在使用运算符号或者对比符时,会自带隐式转换,规则如下:

转化规则

  • -*/% :一律转换成数值后计算
  • +
    • 数字 + 字符串 = 字符串, 运算顺序是从左到右
    • 数字 + 对象, 优先调用对象的 valueOf -> toString
    • 数字 + boolean/null -> 数字
    • 数字 + undefined -> NaN
  • [1].toString() === '1'
  • {}.toString() === '[object object]'
  • NaN !== NaN 、+ undefinedNaN

在 JS 中类型转换只有三种情况,分别是:

  • 转换为布尔值
  • 转换为数字
  • 转换为字符串

Boolean

在条件判断时,除了 undefinednullfalseNaN''0-0,其他所有值都转为 true,包括所有对象

对象转原始类型是根据什么流程运行的

对象转原始类型,会调用内置的[ToPrimitive]函数,对于该函数而言,其逻辑如下:

  • 如果有 Symbol.toPrimitive() 方法,优先调用再返回
  • 调用 valueOf() ,如果转换为原始类型,则返回
  • 调用 toString() ,如果转换为原始类型,则返回
  • 如果都没有返回原始类型,会报错
1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = {
value: 3,
valueOf() {
return 4;
},
toString() {
return '5'
},
[Symbol.toPrimitive]() {
return 6
}
}
console.log(obj + 1); // 输出7

如何让if(a == 1 && a == 2)条件成立

其实就是上一个问题的应用

1
2
3
4
5
6
7
8
const a = {
value: 0,
valueOf: function() {
this.value++;
return this.value;
}
};
console.log(a == 1 && a == 2);//true

四则运算符

它有以下几个特点:

  • 运算中其中一方为字符串,那么就会把另一方也转换为字符串
  • 如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
    1
    2
    3
    1 + '1' // '11'
    true + true // 2
    4 + [1,2,3] // "41,2,3"
    • 对于第一行代码来说,触发特点一,所以将数字 1 转换为字符串,得到结果 ‘11’
    • 对于第二行代码来说,触发特点二,所以将 true 转为数字 1
    • 对于第三行代码来说,触发特点二,所以将数组通过 toString 转为字符串 1,2,3,得到结果 41,2,3

另外对于加法还需要注意这个表达式 ‘a’ + + ‘b’

1
'a' + + 'b' // -> "aNaN"
  • 因为 + ‘b’ 等于 NaN,所以结果为 “aNaN”,你可能也会在一些代码中看到过 + '1’的形式来快速获取 number 类型。
  • 那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字
1
2
3
4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN

比较运算符

  • 如果是对象,就通过 toPrimitive 转换对象
  • 如果是字符串,就通过 unicode 字符索引来比较
1
2
3
4
5
6
7
8
9
10
11
let a = {
valueOf() {
return 0
},
toString() {
return '1'
}
}
a > -1 // true

// 在以上代码中,因为 a 是对象,所以会通过 valueOf 转换为原始类型再比较值。

[] == ![]结果是什么?为什么?

  • == 中,左右两边都需要转换为数字然后进行比较
  • [] 转换为数字为 0
  • ![] 首先是转换为布尔值,由于 [] 作为一个引用类型转换为布尔值为 true, 因此 ![]false ,进而在转换成数字,变为 0
  • 0 == 0 , 结果为 true

== 和 ===有什么区别

=== 叫做严格相等,是指:左右两边不仅值要相等,类型也要相等,例如 '1'===1 的结果是 false ,因为一边是 string ,另一边是 number

==不像===那样严格,对于一般情况,只要值相等,就返回true,但==还涉及一些类型转换,它的转换规则如下

  • 两边的类型是否相同,相同的话就比较值的大小,例如1==2,返回false
  • 判断的是否是null和undefined,是的话就返回true
  • 判断的类型是否是String和Number,是的话,把String类型转换成Number,再进行比较
  • 判断其中一方是否是Boolean,是的话就把Boolean转换成Number`,再进行比较
  • 如果其中一方为Object,且另一方为String、Number或者Symbol,会将Object转换成字符串,再进行比较

闭包

红宝书(p178)上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数,MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。(其中自由变量,指在函数中使用的,但既不是函数参数arguments也不是函数的局部变量的变量,其实就是另外一个函数作用域中的变量。)

产生原因

首先要明白作用域链的概念,其实很简单,在ES5中只存在两种作用域————全局作用域函数作用域,当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链,值得注意的是,每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。 比如:

1
2
3
4
5
6
7
8
var a = 1;
function f1() {
var a = 2
function f2() {
var a = 3;
console.log(a);//3
}
}

在这段代码中,f1的作用域指向有全局作用域(window)和它本身,而f2的作用域指向全局作用域(window)、f1和它本身。而且作用域是从最底层向上找,直到找到全局作用域window为止,如果全局还没有的话就会报错。就这么简单一件事情

闭包产生的本质就是,当前环境中存在指向父级作用域的引用。还是举上面的例子:

1
2
3
4
5
6
7
8
9
function f1() {
var a = 2
function f2() {
console.log(a);//2
}
return f2;
}
var x = f1();
x();

这里x会拿到父级作用域中的变量,输出2。因为在当前环境中,含有对f2的引用,f2恰恰引用了window、f1和f2的作用域。因此f2可以访问到f1的作用域的变量。

那是不是只有返回函数才算是产生了闭包呢?回到闭包的本质,我们只需要让父级作用域的引用存在即可,因此我们还可以这么做:

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

f1执行,给f3赋值后,等于说现在f3拥有了windowf1f3本身这几个作用域的访问权限,还是自底向上查找,最近是在f1中找到了a,因此输出2。
在这里是外面的变量f3存在着父级作用域的引用,因此产生了闭包,形式变了,本质没有改变

表现形式

明白了本质之后,我们就来看看,在真实的场景中,究竟在哪些地方能体现闭包的存在?

  • 返回一个函数。刚刚已经举例。
  • 作为函数参数传递
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var a = 1;
    function foo(){
    var a = 2;
    function baz(){
    console.log(a);
    }
    bar(baz);
    }
    function bar(fn){
    // 这就是闭包
    fn();
    }
    // 输出2,而不是1
    foo();
  • 在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 定时器
    setTimeout(function timeHandler() {
    console.log('111');
    }, 100)

    // 事件监听
    $('#app').click(function(){
    console.log('DOM Listener');
    })
  • IIFE (立即执行函数表达式)创建闭包, 保存了全局作用域window和当前函数的作用域,因此可以访问全局的变量
    1
    2
    3
    4
    5
    var a = 2;
    (function IIFE() {
    // 输出2
    console.log(a);
    })();

如何解决下面的循环输出问题

1
2
3
4
5
for (var i = 1; i <= 5; i ++) {
setTimeout(function timer() {
console.log(i)
}, 0)
}

为什么会全部输出6?如何改进,让它输出1,2,3,4,5?(方法越多越好) 因为setTimeout为宏任务,由于JS中单线程eventLoop机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后setTimeout中的回调才依次执行,但输出i的时候当前作用域没有,往上一级再找,发现了i,此时循环已经结束,i变成了6。因此会全部输出6。

解决方法:

  1. 利用IIFE(立即执行函数表达式)当每次for循环时,把此时的i变量传递到定时器中
    1
    2
    3
    4
    5
    6
    7
    for(var i = 1;i <= 5;i++){
    (function(j){
    setTimeout(function timer(){
    console.log(j)
    }, 0)
    })(i)
    }
  2. 给定时器传入第三个参数, 作为 timer 函数的第一个函数参数
    1
    2
    3
    4
    5
    for(var i=1;i<=5;i++){
    setTimeout(function timer(j){
    console.log(j)
    }, 0, i)
    }
  3. 使用ES6中的let
    1
    2
    3
    4
    5
    for(let i = 1; i <= 5; i++){
    setTimeout(function timer(){
    console.log(i)
    },0)
    }
    let 使 JS 发生革命性的变化,让JS有函数作用域变为了块级作用域,用let后作用域链不复存在。代码的作用域以块级为单位,以上面代码为例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // i = 1
    {
    setTimeout(function timer(){
    console.log(1)
    },0)
    }
    // i = 2
    {
    setTimeout(function timer(){
    console.log(2)
    },0)
    }
    // i = 3
    ...

原型链

原型/构造函数/实例

  • 原型(prototype): 一个简单的对象,用于实现对象的 属性继承。可以简单的理解成对象的爹。在 FirefoxChrome 中,每个JavaScript对象中都包含一个__proto__(非标准)的属性指向它爹(该对象的原型),可obj.__proto__进行访问。
  • 构造函数: 可以通过 new 来 新建一个对象 的函数。
  • 实例: 通过构造函数和 new 创建出来的对象,便是实例。 实例通过 __proto__ 指向原型,通过 constructor 指向构造函数。

Object 为例,我们常用的 Object 便是一个构造函数,因此我们可以通过它构建实例。
则此时, 实例为 instance , 构造函数为 Object ,我们知道,构造函数拥有一个 prototype 的属性指向原型,因此原型为:

1
2
3
4
5
// 实例
const instance = new Object()

// 原型
const prototype = Object.prototype

这里我们可以来看出三者的关系:

  • 实例.__proto__ === 原型
  • 原型.constructor === 构造函数
  • 构造函数.prototype === 原型

实例.constructor === 构造函数

1
2
3
4
5
6
7
// 这条线其实是是基于原型进行获取的,可以理解成一条基于原型的映射线
// 例如:
// const o = new Object()
// o.constructor === Object --> true
// o.__proto__ = null;
// o.constructor === Object --> false

原型对象和构造函数有何关系

  • 在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会天生自带一个prototype属性,这个属性指向函数的原型对象。
  • 当函数经过new调用时,这个函数就成为了构造函数,返回一个全新的实例对象,这个实例对象有一个__proto__属性,指向构造函数的原型对象。
    原型对象和构造函数有何关系

描述原型链

JavaScript 对象通过 __proto__ 指向父类对象,直到指向 Object 对象为止,这样就形成了一个原型指向的链条, 即原型链

描述原型链
  • 对象的 hasOwnProperty() 来检查对象自身中是否含有该属性
  • 使用 in 检查对象中是否含有某个属性时,如果对象中没有但是原型链中有,也会返回 true

继承

借助call

这样写的时候子类虽然能够拿到父类的属性值,但是问题是父类原型对象中一旦存在方法那么子类无法继承。那么引出下面的方法。

1
2
3
4
5
6
7
8
function Parent1(){
this.name = 'parent1';
}
function Child1(){
Parent1.call(this);
this.type = 'child1'
}
console.log(new Child1);

借助原型链

1
2
3
4
5
6
7
8
9
10
function Parent2() {
this.name = 'parent2';
this.play = [1, 2, 3]
}
function Child2() {
this.type = 'child2';
}
Child2.prototype = new Parent2();

console.log(new Child2());

this

我们先来看几个函数调用的场景

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

const obj = {
a: 2,
foo: foo
}
obj.foo()

const c = new foo()

箭头函数中的 this

1
2
3
4
5
6
7
8
function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
  • 首先箭头函数其实是没有 this 的,箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this 。在这个例子中,因为包裹箭头函数的第一个普通函数是 a ,所以此时的 thiswindow 。另外对箭头函数使用 bind 这类函数是无效的。
  • 最后种情况也就是 bind 这些改变上下文的 API 了,对于这些函数来说,this 取决于第一个参数,如果第一个参数为空,那么就是 window
  • 那么说到 bind ,不知道大家是否考虑过,如果对一个函数进行多次 bind ,那么上下文会是什么呢?
1
2
3
let a = {}
let fn = function () { console.log(this) }
fn.bind().bind(a)() // => ?

如果你认为输出结果是 a ,那么你就错了,其实我们可以把上述代码转换成另一种形式

1
2
3
4
5
6
7
// fn.bind().bind(a) 等于
let fn2 = function fn1() {
return function() {
return fn.apply()
}.apply(a)
}
fn2()

可以从上述代码中发现,不管我们给函数 bind 几次,fn 中的 this 永远由第一次 bind 决定,所以结果永远是 window

1
2
3
4
5
let a = { name: 'poetries' }
function foo() {
console.log(this.name)
}
foo.bind(a)() // => 'poetries'

以上就是 this 的规则了,但是可能会发生多个规则同时出现的情况,这时候不同的规则之间会根据优先级最高的来决定 this 最终指向哪里。
首先,new 的方式优先级最高,接下来是 bind 这些函数,然后是 obj.foo() 这种调用方式,最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。

this执行主体,谁把它执行的和在哪创建的在哪执行的都没有必然的关系

总结

this执行主体,谁把它执行的和在哪创建的在哪执行的都没有必然的关系

  • 函数执行,看方法前面是否有点,没有点 thiswindow (严格模式下是 undefined ),有点,点前面是谁· this ·就是谁
  • 给当前元素的某个事件行为绑定方法,当事件行为触发,方法中的 this 是当前元素本身(排除 attachEvent
  • 构造函数体中 this 是当前类的实例
  • 箭头函数中没有执行主体,所用到的 this 都是所处上下文中的 this
  • 可以基于 Function.prototype 上的 call/apply/bind 改变 this 指向

内存机制

基本数据类型用栈存储,引用数据( 对象 )类型用堆存储。闭包变量是存在堆内存中的。

具体而言,以下数据类型存储在栈中:

  • boolean
  • null
  • undefined
  • number
  • string
  • symbol
  • bigint
1
2
3
4
let obj = { a: 1 };
let obj1 = obj;
obj1.a = 2;
console.log(obj.a);//变成了2

之所以会这样,是因为 objobj1 是同一份堆空间的地址,改变 obj1 ,等于改变了共同的堆内存,这时候通过 obj 来获取这块内存的值当然会改变。 当然,你可能会问: 为什么不全部用栈来保存呢?
首先,对于系统栈来说,它的功能除了保存变量之外,还有创建并切换函数执行上下文的功能。举个例子:

1
2
3
4
5
6
7
8
9
function f(a) {
console.log(a);
}

function func(a) {
f(a);
}

func(1);
  • 假设用 ESP 指针来保存当前的执行状态,在系统栈中会产生如下的过程:
  1. 调用 func , 将 func 函数的上下文压栈,ESP 指向栈顶。
  2. 执行 func ,又调用 f 函数,将 f 函数的上下文压栈,ESP 指针上移。
  3. 执行完 f 函数,将 ESP 下移,f 函数对应的栈顶空间被回收。
  4. 执行完 func ,ESP 下移,func 对应的空间被回收。
在系统栈中会产生如下的过程

因此你也看到了,如果采用栈来存储相对基本类型更加复杂的对象数据,那么切换上下文的开销将变得巨大!
不过堆内存虽然空间大,能存放大量的数据,但与此同时垃圾内存的回收会带来更大的开销

执行上下文

当执行 JS 代码时,会产生三种执行上下文

  • 全局执行上下文
  • 函数执行上下文
  • eval 执行上下文
    每个执行上下文中都有三个重要的属性
  • 变量对象(VO),包含变量、函数声明和函数的形参,该属性只能在全局上下文中访问
  • 作用域链(JS 采用词法作用域,也就是说变量的作用域是在定义时就决定了)
  • this
1
2
3
4
5
var a = 10
function foo(i) {
var b = 20
}
foo()

对于上述代码,执行栈中有两个上下文:全局上下文和函数 foo 上下文。

1
2
3
4
5
6
7
8
b() // call b
console.log(a) // undefined

var a = 'Hello world'

function b() {
console.log('call b')
}

想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行上下文时,会有两个阶段。第一个阶段是创建的阶段(具体步骤是创建 VO),JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined ,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用。

在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升

1
2
3
4
5
6
7
8
9
b() // call b second

function b() {
console.log('call b fist')
}
function b() {
console.log('call b second')
}
var b = 'Hello world'

var 会产生很多错误,所以在 ES6中引入了 letlet不能在声明前使用,但是这并不是常说的 let 不会提升,let 提升了声明但没有赋值,因为临时死区导致了并不能在声明前使用。

对于非匿名的立即执行函数需要注意以下一点

1
2
3
4
5
var foo = 1
(function foo() {
foo = 10
console.log(foo)
}()) // -> ƒ foo() { foo = 10 ; console.log(foo) }

因为当 JS 解释器在遇到非匿名的立即执行函数时,会创建一个辅助的特定对象,然后将函数名称作为这个对象的属性,因此函数内部才可以访问到 foo,但是这个值又是只读的,所以对它的赋值并不生效,所以打印的结果还是这个函数,并且外部的值也没有发生更改。

小结

执行上下文可以简单理解为一个对象

  • 它包含三个部分:
    • 变量对象(VO)
    • 作用域链(词法作用域)
    • this指向
  • 它的类型:
    • 全局执行上下文
    • 函数执行上下文
    • eval 执行上下文
  • 代码执行过程:
    • 创建 全局上下文 (global EC)
    • 全局执行上下文 (caller) 逐行 自上而下 执行。遇到函数时,函数执行上下文 (callee) 被push到执行栈顶层
    • 函数执行上下文被激活,成为 active EC, 开始执行函数中的代码,caller 被挂起
    • 函数执行完后,calleepop移除出执行栈,控制权交还全局上下文 (caller),继续执行

变量提升

当执行 JS 代码时,会生成执行环境,只要代码不是写在函数中的,就是在全局执行环境中,函数中的代码会产生函数执行环境,只此两种执行环境。

1
2
3
4
5
6
7
8
b() // call b
console.log(a) // undefined

var a = 'Hello world'

function b() {
console.log('call b')
}

想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行环境时,会有两个阶段。第一个阶段是创建的阶段,JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用

在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升

1
2
3
4
5
6
7
8
9
b() // call b second

function b() {
console.log('call b fist')
}
function b() {
console.log('call b second')
}
var b = 'Hello world'

var 会产生很多错误,所以在 ES6中引入了 letlet不能在声明前使用,但是这并不是常说的 let 不会提升,let 提升了,在第一阶段内存也已经为他开辟好了空间,但是因为这个声明的特性导致了并不能在声明前使用

模块化

模块化开发在现代开发中已是必不可少的一部分,它大大提高了项目的可维护、可拓展和可协作性。通常,我们 在浏览器中使用 ES6 的模块化支持,在 Node 中使用 commonjs 的模块化支持。
分类:

  • es6: import / export
  • commonjs: require / module.exports / exports
  • amd: require / defined
    require与import的区别
  • require 支持 动态导入,import 不支持,正在提案 ( babel 下可支持)
  • require 是 同步 导入,import 属于 异步 导入
  • require 是 值拷贝,导出值变化不会影响导入值;import 指向 内存地址,导入值会随导出值而变化

setTimeout、Promise、Async / Await 的区别

  • 首先,我们先来了解一下基本概念:
    • js EventLoop 事件循环机制:
    • JavaScript的事件分两种,宏任务(macro-task)和微任务(micro-task)
  • 宏任务:包括整体代码script,setTimeoutsetInterval
  • 微任务:Promise.then(非new Promise),process.nextTick(node中)
  • 事件的执行顺序,是先执行宏任务,然后执行微任务,这个是基础,任务可以有同步任务和异步任务,同步的进入主线程,异步的进入Event Table并注册函数,异步事件完成后,会将回调函数放入Event Queue中(宏任务和微任务是不同的Event Queue),同步任务执行完成后,会从Event Queue中读取事件放入主线程执行,回调函数中可能还会包含不同的任务,因此会循环执行上述操作。
  • 注意: setTimeOut 并不是直接的把你的回掉函数放进上述的异步队列中去,而是在定时器的时间到了之后,把回掉函数放到执行异步队列中去。如果此时这个队列已经有很多任务了,那就排在他们的后面。这也就解释了为什么 setTimeOut 为什么不能精准的执行的问题了。
  • setTimeout 执行需要满足两个条件:
    • 主进程必须是空闲的状态,如果到时间了,主进程不空闲也不会执行你的回掉函数
    • 这个回掉函数需要等到插入异步队列时前面的异步函数都执行完了,才会执行
  • 上面是比较官方的解释,说一下自己的理解吧:
    • 了解了什么是宏任务和微任务,就好理解多了,首先执行 宏任务 => 微任务的Event Queue => 宏任务的Event Queue
  • promiseasync/await
    • 首先,new Promise是同步的任务,会被放到主进程中去立即执行。而.then()函数是异步任务会放到异步队列中去,那什么时候放到异步队列中去呢?当你的promise状态结束的时候,就会立即放进异步队列中去了。
    • async关键字的函数会返回一个promise对象,如果里面没有await,执行起来等同于普通函数;如果没有await,async函数并没有很厉害是不是
    • await 关键字要在 async 关键字函数的内部,await 写在外面会报错;await如同他的语意,就是在等待,等待右侧的表达式完成。此时的await会让出线程,阻塞async内后续的代码,先去执行async外的代码。等外面的同步代码执行完毕,才会执行里面的后续代码。就算await的不是promise对象,是一个同步函数,也会等这样操作
宏任务 Event Queue -流程图-微任务 Event Queue

根据图片显示我们来整理一下流程:

  • 执行console.log('script start'),输出script start
  • 执行setTimeout,是一个异步动作,放入宏任务异步队列中;
  • 执行async1(),输出async1 start,继续向下执行;
  • 执行async2(),输出async2,并返回了一个promise对象,await让出了线程,把返回的promise加入了微任务异步队列,所以async1()下面的代码也要等待上面完成后继续执行;
  • 执行 new Promise,输出promise1,然后将resolve放入微任务异步队列;
  • 执行console.log('script end'),输出script end
  • 到此同步的代码就都执行完成了,然后去微任务异步队列里去获取任务
  • 接下来执行resolveasync2返回的promise返回的),输出了async1 end
  • 然后执行resolvenew Promise的),输出了promise2
  • 最后执行setTimeout,输出了settimeout

async原理

async/await 语法糖就是使用 Generator 函数+自动执行器来运作的

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
// 定义了一个promise,用来模拟异步请求,作用是传入参数++
function getNum(num){
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(num+1)
}, 1000)
})
}

//自动执行器,如果一个Generator函数没有执行完,则递归调用
function asyncFun(func){
var gen = func();

function next(data){
var result = gen.next(data);
if (result.done) return result.value;
result.value.then(function(data){
next(data);
});
}

next();
}

// 所需要执行的Generator函数,内部的数据在执行完成一步的promise之后,再调用下一步
var func = function* (){
var f1 = yield getNum(1);
var f2 = yield getNum(f1);
console.log(f2) ;
};
asyncFun(func);

JS 整数

通过 Number 类型来表示,遵循 IEEE754 标准,通过 64 位来表示一个数字,(1 + 11 + 52),最大安全数字是 Math.pow(2, 53) - 1,对于 16 位十进制。(符号位 + 指数位 + 小数部分有效位)
Math.pow(2, 53) ,53 为有效数字,会发生截断,等于 JS 能支持的最大数字。

setTimeout(fn, 0)多久才执行,Event Loop

setTimeout 按照顺序放到队列里面,然后等待函数调用栈清空之后才开始执行,而这些操作进入队列的顺序,则由设定的延迟时间来决定

js脚本加载问题,async、defer问题

  • 如果依赖其他脚本和 DOM 结果,使用 defer
  • 如果与 DOM 和其他脚本依赖不强时,使用 async
    script 引入方式
  • html 静态<script>引入
  • js 动态插入<script>
  • <script defer>: 异步加载,元素解析完成后执行
  • <script async>: 异步加载,但执行时会阻塞元素渲染

垃圾回收机制

  • 对于在JavaScript中的字符串,对象,数组是没有固定大小的,只有当对他们进行动态分配存储时,解释器就会分配内存来存储这些数据,当JavaScript的解释器消耗完系统中所有可用的内存时,就会造成系统崩溃。
  • 内存泄漏,在某些情况下,不再使用到的变量所占用内存没有及时释放,导致程序运行中,内存越占越大,极端情况下可以导致系统崩溃,服务器宕机。
  • JavaScript有自己的一套垃圾回收机制,JavaScript的解释器可以检测到什么时候程序不再使用这个对象了(数据),就会把它所占用的内存释放掉。
  • 针对JavaScript的来及回收机制有以下两种方法(常用):标记清除,引用计数
  • 标记清除

几种类型的DOM节点

  • Document 节点,整个文档是一个文档节点;
  • Element 节点,每个 HTML 标签是一个元素节点;
  • Attribute 节点,每一个 HTML 属性是一个属性节点;
  • Text 节点,包含在 HTML 元素中的文本是文本节点

对象的创建方式

工厂模式,创建方式

1
2
3
4
5
6
7
8
9
10
11
function createPerson(name,age,job){
var o = new Object();
o.name=name;
o.age=age;
o.job=job;
o.sayName = function(){
alert(this.name);
}
}
var person1 = createPerson("da",1,"it");
var person2 = createPerson("dada",2,"it");

构造函数模式

1
2
3
4
5
6
7
8
9
10
function Person(name,age,ob){
this.name=name;
this.age=age;
this.job=job;
this.sayName = function(){
alert(this.name);
}
var person1 = new Person("dada",1,"web");
var person2 = new Person("dada",2,"web");
}

原型模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person() {}
Person.prototype.name = "da";
Person.prototype.age = 1;
Person.prototype.job = "web";
Person.prototype.sayName = function(){
alert(this.name);
}

var person1 = new Person();
person1.sayName(); //"dada"

var person2 = new Person();
person2.sayName(); //"dada"

alert(person1.sayName == person2.sayName); //true

组合使用构造函数模式和原型模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person(name,age){
this.name = name;
this.age = age;
this.friends = ["da","dada"];
}
Person.prototype = {
constructor:Person,
sayName:function(){
alert(this.name);
}
}
var person1 = new Person("da1",1);
var person2 = new Person("da2",2);
person1.friends.push("dadada");
console.log(person1.friends); //["da","dada","dadada"]
console.log(person2.friends); //["da","dada"]
console.log(person1.friends === person2.friends); //false
console.log(person1.sayName === person2.sayName); //true

动态原型模式

1
2
3
4
5
6
7
8
9
10
11
function Person(name,age,job){
this.name=name;
this.age=age;
this.job=job;

if(typeof this.sayName!="function"){
Person.prototype.sayName=function(){
alert(this.name);
};
}
}

转化类数组成数组

因为arguments本身并不能调用数组方法,它是一个另外一种对象类型,只不过属性从0开始排,依次为0,1,2…最后还有callee和length属性。我们也把这样的对象称为类数组

常见的类数组还有:

  • getElementsByTagName/ClassName()获得的HTMLCollection
  • querySelector获得的nodeList
  1. Array.prototype.slice.call()
1
2
3
4
5
function sum(a, b) {
let args = Array.prototype.slice.call(arguments);
console.log(args.reduce((sum, cur) => sum + cur));//args可以调用数组原生的方法啦
}
sum(1, 2);//3
  1. Array.from()
1
2
3
4
5
6
function sum(a, b) {
let args = Array.from(arguments);
console.log(arguments, args)
console.log(args.reduce((sum, cur) => sum + cur));//args可以调用数组原生的方法啦
}
sum(1, 2);//3
  1. ES6展开运算符
1
2
3
4
5
function sum(a, b) {
let args = [...arguments];
console.log(args.reduce((sum, cur) => sum + cur));//args可以调用数组原生的方法啦
}
sum(1, 2);//3
  1. concat + apply
1
2
3
4
5
function sum(a, b) {
let args = Array.prototype.concat.apply([], arguments);//apply方法会把第二个参数展开
console.log(args.reduce((sum, cur) => sum + cur));//args可以调用数组原生的方法啦
}
sum(1, 2);//3

中断forEach

forEach 中用 return 不会返回,函数会继续执行。

1
2
3
4
let nums = [1, 2, 3];
nums.forEach((item, index) => {
return;//无效
})

中断方法:

  • 使用 try 监视代码块,在需要中断的地方抛出异常。
  • 官方推荐方法(替换方法):用everysome替代forEach函数。every在碰到return false的时候,中止循环。some在碰到return true的时候,中止循环

数组中是否包含某个值

  • array.indexOf 此方法判断数组中是否存在某个值,如果存在,则返回数组元素的下标,否则返回-1。
    1
    2
    3
    var arr = [1,2,3,4];
    var index = arr.indexOf(3);
    console.log(index);
  • array.includes(searcElement[,fromIndex]) 数组中是否存在某个值,如果存在返回true,否则返回false
    1
    2
    3
    4
    5
    var arr = [1,2,3,4];
    if(arr.includes(3))
    console.log("存在");
    else
    console.log("不存在");
  • array.find(callback[,thisArg]) 返回数组中满足条件的第一个元素的值,如果没有,返回undefined
    1
    2
    3
    4
    5
    var arr=[1,2,3,4];
    var result = arr.find(item =>{
    return item > 3
    });
    console.log(result);
  • array.findeIndex(callback[,thisArg]) 返回数组中满足条件的第一个元素的下标,如果没有找到,返回-1
    1
    2
    3
    4
    5
    var arr=[1,2,3,4];
    var result = arr.findIndex(item =>{
    return item > 3
    });
    console.log(result);

数组扁平化

1
let ary = [1, [2, [3, [4, 5]]], 6];// -> [1, 2, 3, 4, 5, 6]

需求:多维数组=>一维数组

  • flat 方法
    1
    ary = ary.flat(Infinity);
  • replace + split
    1
    ary = str.replace(/(\[|\])/g, '').split(',')
  • replace + JSON.parse
    1
    2
    str = str.replace(/(\[|\])/g, '');
    ary = JSON.parse('[' + str + ']');
  • 普通递归
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let result = [];
    let fn = function(ary) {
    for(let i = 0; i < ary.length; i++) {
    let item = ary[i];
    if (Array.isArray(ary[i])){
    fn(item);
    } else {
    result.push(item);
    }
    }
    }
  • reduce 迭代
    1
    2
    3
    4
    5
    6
    7
    function flatten(ary) {
    return ary.reduce((pre, cur) => {
    return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
    }, []);
    }
    let ary = [1, 2, [3, 4], [5, [6, 7]]]
    console.log(flatten(ary))
  • 扩展运算符(...
1
2
3
4
//只要有一个元素有数组,那么循环继续
while (ary.some(Array.isArray)) {
ary = [].concat(...ary);
}

中浅拷贝

什么是拷贝?

首先来直观的感受一下什么是拷贝

1
2
3
4
5
6
let arr = [1, 2, 3];
let newArr = arr;
newArr[0] = 100;

console.log(arr);//[100, 2, 3]
// 这是直接赋值的情况,不涉及任何拷贝。当改变newArr的时候,由于是同一个引用,arr指向的值也跟着改变。
  • 浅拷贝:
    1. Object.assign() 任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象

      1
      2
      3
      4
      5
      let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
      let obj2 = Object.assign({}, obj1);
      obj2.person.name = "wade";
      obj2.sports = 'football'
      console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }
    2. 函数库lodash的_.clone方法 该函数库也有提供_.clone用来做 Shallow Copy,后面我们会再介绍利用这个库实现深拷贝。

      1
      2
      3
      4
      5
      6
      7
      8
      var _ = require('lodash');
      var obj1 = {
      a: 1,
      b: { f: { g: 1 } },
      c: [1, 2, 3]
      };
      var obj2 = _.clone(obj1);
      console.log(obj1.b.f === obj2.b.f);// true
    3. 展开运算符(...) ES6特性,它提供了一种非常方便的方式来执行浅拷贝, 这与 Object.assign() 的功能相同

      1
      2
      3
      4
      5
      let obj1 = { name: 'Kobe', address:{x:100,y:100}}
      let obj2= {... obj1}
      obj1.address.x = 200;
      obj1.name = 'wade'
      console.log('obj2',obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }
    4. Array.prototype.concat()

      1
      2
      3
      4
      5
      6
      let arr = [1, 3, {
      username: 'kobe'
      }];
      let arr2 = arr.concat();
      arr2[2].username = 'wade';
      console.log(arr); //[ 1, 3, { username: 'wade' } ]
    5. Array.prototype.slice()

      1
      2
      3
      4
      5
      6
      let arr = [1, 3, {
      username: ' kobe'
      }];
      let arr3 = arr.slice();
      arr3[2].username = 'wade'
      console.log(arr); // [ 1, 3, { username: 'wade' } ]
  • 深拷贝:
    • JSON.parse(JSON.stringify()) 不能处理函数和正则
      1
      2
      3
      4
      5
      6
      let arr = [1, 3, {
      username: ' kobe'
      }];
      let arr4 = JSON.parse(JSON.stringify(arr));
      arr4[2].username = 'duncan';
      console.log(arr, arr4)
    • 函数库lodash_.cloneDeep方法
      1
      2
      3
      4
      5
      6
      7
      8
      var _ = require('lodash');
      var obj1 = {
      a: 1,
      b: { f: { g: 1 } },
      c: [1, 2, 3]
      };
      var obj2 = _.cloneDeep(obj1);
      console.log(obj1.b.f === obj2.b.f);// false
    • jQuery.extend() 方法
      1
      2
      3
      4
      5
      6
      7
      8
      var $ = require('jquery');
      var obj1 = {
      a: 1,
      b: { f: { g: 1 } },
      c: [1, 2, 3]
      };
      var obj2 = $.extend(true, {}, obj1);
      console.log(obj1.b.f === obj2.b.f); // false
    • 手写递归 遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      function deepClone(obj, hash = new WeakMap()) {
      if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
      if (obj instanceof Date) return new Date(obj);
      if (obj instanceof RegExp) return new RegExp(obj);
      // 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
      if (typeof obj !== "object") return obj;
      // 是对象的话就要进行深拷贝
      if (hash.get(obj)) return hash.get(obj);
      let cloneObj = new obj.constructor();
      // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
      hash.set(obj, cloneObj);
      for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
      // 实现一个递归拷贝
      cloneObj[key] = deepClone(obj[key], hash);
      }
      }
      return cloneObj;
      }
      let obj = { name: 1, address: { x: 100 } };
      obj.o = obj; // 对象存在循环引用的情况
      let d = deepClone(obj);
      obj.address.x = 200;
      console.log(d);

数组(array)

  • map: 遍历数组,返回回调返回值组成的新数组
  • forEach: 无法break,可以用try/catchthrow new Error来停止
  • filter: 过滤
  • some: 有一项返回true,则整体为true
  • every: 有一项返回false,则整体为false
  • join: 通过指定连接符生成字符串
  • push / pop: 末尾推入和弹出,改变原数组, 返回推入/弹出项
  • unshift / shift: 头部推入和弹出,改变原数组,返回操作项
  • sort(fn) / reverse: 排序与反转,改变原数组
  • concat: 连接数组,不影响原数组, 浅拷贝
  • slice(start, end): 返回截断后的新数组,不改变原数组
  • splice(start, number, value...): 返回删除元素组成的数组,value为插入项,改变原数组
  • indexOf / lastIndexOf(value, fromIndex): 查找数组项,返回对应的下标
  • reduce / reduceRight(fn(prev, cur), defaultPrev): 两两执行,prev 为上次化简函数的return值,cur为当前值(从第二项开始)
    数组乱序:
1
2
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
arr.sort(() => Math.random() - 0.5);

数组拆解: flat: [1,[2,3]] --> [1, 2, 3]

1
Array.prototype.flat = () => this.toString().split(',').map(item => +item)

Array(3)和Array(3, 4)的区别

1
2
console.log(Array(3)) // [empty x 3]
console.log(Array(3, 4)) // [3, 4]

请创建一个长度为100,值都为1的数组

1
new Array(100).fill(1)

请创建一个长度为100,值为对应下标的数组

1
2
3
4
5
6
// cool的写法:
[...Array(100).keys()]

// 其他方法:
Array(100).join(",").split(",").map((v, i) => i)
Array(100).fill().map((v, i) => i)

代码的复用

当你发现任何代码开始写第二遍时,就要开始考虑如何复用。一般有以下的方式:

  • 函数封装
  • 继承
  • 复制 extend
  • 混入 mixin
  • 借用 apply/call

DOM节点

创建新节点

1
2
3
createDocumentFragment()    //创建一个DOM片段
createElement() //创建一个具体的元素
createTextNode() //创建一个文本节点

添加、移除、替换、插入

1
2
3
4
appendChild()      //添加
removeChild() //移除
replaceChild() //替换
insertBefore() //插入

查找

1
2
3
getElementsByTagName()    //通过标签名称
getElementsByName() //通过元素的Name属性的值
getElementById() //通过元素Id,唯一性

Ajax

  • Ajax 的原理简单来说是在用户和服务器之间加了—个中间层( AJAX 引擎),通过 XmlHttpRequest 对象来向服务器发异步请求,从服务器获得数据,然后用javascript 来操作 DOM 而更新页面。使用户操作与服务器响应异步化。这其中最关键的一步就是从服务器获得请求数据
  • Ajax 的过程只涉及 JavaScriptXMLHttpRequestDOM 。XMLHttpRequestajax 的核心机制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. 创建连接
var xhr = null;
xhr = new XMLHttpRequest()
// 2. 连接服务器
xhr.open('get', url, true)
// 3. 发送请求
xhr.send(null);
// 4. 接受请求
xhr.onreadystatechange = function(){
if(xhr.readyState == 4){
if(xhr.status == 200){
success(xhr.responseText);
} else { // fail
fail && fail(xhr.status);
}
}
}
  • 优点:

    • 通过异步模式,提升了用户体验.
    • 优化了浏览器和服务器之间的传输,减少不必要的数据往返,减少了带宽占用.
    • Ajax在客户端运行,承担了一部分本来由服务器承担的工作,减少了大用户量下的服务器负载。
    • Ajax可以实现动态不刷新(局部刷新)
  • 缺点:

    • 安全问题 AJAX暴露了与服务器交互的细节。
    • 对搜索引擎的支持比较弱。
    • 不容易调试。

for in/for of

for in 性能很差,迭代当前对象中可枚举的属性,并且一直查找到原型上去。

  • 问题1:遍历顺序数字优先
  • 问题2:无法遍历symbol属性
  • 问题3:可以遍历到原型属性中可枚举的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    let obj = {
    name: 'poetry',
    age: 22,
    [Symbol('aa')]: 100,
    0: 200,
    1: 300
    }

    for(let key in obj) {
    // 不遍历原型上的属性
    if(!obj.hasOwnProperty(key)) {
    break;
    }
    }

for of

  • 部分数据结构实现了迭代器规范
    • Symbol.itertor
    • 数组/set/map
    • 对象没有实现,for of不能遍历对象
    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
    // 数组具备迭代器规范,模拟实现
    var arr = [1,2,3,4,5]

    arr[Symbol.iterator] = function() {
    let self = this, index = 0;

    return {
    next() {
    if(index > self.length - 1) {
    return {
    done: true,
    value: undefined
    }
    }
    return {
    done: false,
    value: self[index++]
    }
    }
    }
    }

    // 数组具备迭代器规范,模拟实现
    var arr = [1,2,3,4,5]

    arr[Symbol.iterator] = function() {
    let self = this, index = 0;

    return {
    next() {
    if(index > self.length - 1) {
    return {
    done: true,
    value: undefined
    }
    }
    return {
    done: false,
    value: self[index++]
    }
    }
    }
    }