5.1 语言基础

~~~~~~~~ 数据类型 ~~~~~~~

function sayHi() {
  console.log(name);
  console.log(age);
  var name = 'Lydia';
  let age = 21;
}
sayHi()

以上代码的输出是什么?

var, let

输出:

  • undefined

  • ReferenceError: Cannot access 'age' before initialization

解析:

  • var 声明的变量会被预解析,var name 会被提升到作用域的最顶部

    • 所以在第 2 行 console.log(name) 时,name 是存在的,但是没有被赋值

    • 所以第 2 行是输出 undefined

  • 而 let 会有暂时性死区

    • 所以在 let 声明变量之前,都无法使用该变量

var name = 'World!';
(function () {
    if (typeof name === 'undefined') {
        var name = 'Jack';
        console.log('Goodbye ' + name);
    } else {
        console.log('Hello ' + name);
} })();

以上代码的输出是什么?

var

输出:'Goodbye Jack'

解析:用 var 定义的变量“都”会被提升至所有代码的最前面。所以以上代码,实际上类似:

var name = 'World!';
(function () {
    var name;
    if (typeof name === 'undefined') {
        // var name = 'Jack';
        name = 'Jack';
        console.log('Goodbye ' + name);
    } else {
        console.log('Hello ' + name);
} })();

TODO:where 会提升到 where

(function(){
  var x = y = 1;
})();
console.log(y);
console.log(x);

以上代码的输出是什么?

var

输出:

  • 1

  • ReferenceError: x is not defined

解析:在作用域内,var 变量定义和函数定义会被先行提升。

所以,上面的代码,实际上是:

var y; // 而 y 则隐式成了个全局变量
(function(){
  var x;  // 只“变量提升”第一个变量
  // var x = y = 1;
  y = 1;
  x = 1;
})();
console.log(y);
console.log(x);

考察点:var 变量提升、隐式定义全局变量

var a = 11;
function test2(){
    this.a = 22;
    let b = ()=>{
        console.log(this.a)
    }
    b();
}
var x = new test2();

以上代码的输出是什么?

var

输出:22

定义时绑定

for(var i=0;i<5;i++) { 
    setTimeout(function(){ 
        console.log(i);
    },1000);
}
console.log(i)

以上代码的输出是什么?

var

输出:5 个 5

每次 for 循环的时候 setTimeout 都会执行,但是里面的 function 不会被立即执行,而是会放进任务队列。

写一个函数,第 n 秒打印 n
// 1. var + 闭包
for(var i=0;i<5;i++){ 
    (function(i){ 
        setTimeout(function(){
            console.log(i)
        }, 1000*i) 
    })(i)
}

// 2. let
for(let i=0;i<5;i++){ 
    setTimeout(function(){
        console.log(i)
    },1000*i)
}
暂时性死区

块级作用域

在代码块内,使用 let, const 命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”。

symbol

Symbol 是 ES6 的新增属性,代表用给定名称作为唯一标识,这种类型的值可以这样创建,let id=symbol('id')

symbl 确保唯一,即使采用相同的名称,也会产生不同的值,我们创建一个字段,仅为知道对应 symbol 的人能访问。

symbol 并不是 100% 隐藏:

  • 有内置方法 Object.getOwnPropertySymbols(obj) 可以获得所有的 symbol。

  • 也有一个方法 Reflect.ownKeys(obj) 返回对象所有的键,包括 symbol

但大多数内置方法和语法结构,都遵循通用约定——它们是隐藏的。

var a = 111111111111111110000
var b = 1111
console.log(a + b)

以上代码的输出是什么?

Number 精度

输出:111111111111111110000(和 a 的值相等)

解析:

对于整数部分,有效数字最多能存 16 位。

// 整数部分,a 的有效数字有 16 位
'111111111111111110000'.length  // 21
a.toExponential()               // 1.1111111111111111e+20

// 而当 a 参加+运算时,它是不安全的
Number.MAX_SAFE_INTEGER         // 9007199254740991
a >= Number.MAX_SAFE_INTEGER    // true
'9007199254740991'.length       // 16

// a + b, 最后 4 位精度刚好被扔了,所以结果值还是 a
1.11111111111111110000e+20
0.00000000000000001111e+20

关于浮点数精度的详情可查阅:https://anjia1.gitbook.io/cs/floating-point/reading

[,,,].join(",")

以上代码的运行结果是什么?

对象字面量

结果:",,"

解析:JavaScript 在使用字面量创建数组时,如果末尾有个 , 会被省略。

所以 [,,,] 实际上只有三个元素 [,,],值都为空即 undefined。而当三个元素使用 join(,) 时,会有两个分隔符 ,

[,,,] // [empty × 3]
function sidEffecting(ary) {
  ary[0] = ary[2]
}
function bar(a,b,c) {
  c = 10
  sidEffecting(arguments)
  return a + b + c
}
console.log(bar(1,1,1))

以上代码的输出是什么?

函数参数 arguments

输出:21

解析:函数内部的 arguments 是个引用,且和变量之间存在着值联动。

  • arguments 是个类数组,有 length 属性的 Object

  • arguments 对象的内部属性是用形参创建的 getter 和 setter 方法,它们任意一个的改变都会影响到另一方

arguments

arguments 是类数组对象。

它有 length 属性,但不能调用数组方法,可用 Array.from() 转换

箭头函数获取 arguments

可用 ...rest 参数获取

判断变量类型的方法
  1. 运算符 typeof

  2. 运算符 instanceof

  3. 属性 constructor

  4. 方法 toString()

typeof a

a instanceof A
a.constructor == A

Object.prototype.toString.call(a)
类型转换规则

x == y 的结果是 true 或者 false

  1. 若 Type(x) 和 Type(y) 相同,则

    1. Type 是 Undefined,返回 true

    2. Type 是 Null,返回 true

    3. Type 是 Boolean,同为 true/false 返回 true,否则返回 false

    4. Type 是 String,当是完全相同的字符序列时返回 true,否则返回 false

    5. Type 是 Number,则

      • 若有一个为 NaN,则返回 false

      • 若是 +0 和 -0,则返回 true

      • 若是相等数值,返回 true

      • 否则返回 false

    6. Type 是对象,若引用的是同一个对象则返回 true,否则返回 false

  2. 一个是 null,一个是 undefined

    • 若 x 是 null,y 是 undefined,则返回 true

    • 若 x 是 undefined,y 是 null,则返回 true

  3. 一个是 String,一个是 Number

    • 若 Type(x) 是 Number,Type(y) 是 String,则返回 x == ToNumber(y) 的结果

    • 若 Type(x) 是 String,Type(y) 是 Number,则返回 ToNumber(x) == y 的结果

  4. 其中一个是 Boolean

    • 若 Type(x) 是 Boolean,返回 ToNumber(x) == y 的结果

    • 若 Type(y) 是 Boolean,返回 x == ToNumber(y) 的结果

  5. 一个是 String 或 Number,一个是 Object

    • 若 Type(x) 是 String 或 Number,Type(y) 是 Object,返回 x == ToPrimitive(y)

    • 若 Type(x) 是 Object,Type(y) 是 String 或 Number,返回 ToPrimitive(x) == y

  6. 返回 false

相关内容

  • ToPrimitive()

    • 若是转成 string,则先调 toString()

    • 若是转成 number,则先调 valueOf()

  • ToNumber()

数据类型

在 JavaScript 中,基本数据类型 = 原始数据类型(值类型) + 对象(引用类型)

  1. 值类型:按值访问,可操作保存在变量中的实际值

  2. 引用类型

    • Object, Array, RegExp, Date, Function

    • 值类型的包装器:String, Number, Boolean

    • 内置对象:Global, Math

~~~~~~~~ 类型转换 ~~~~~~~

其它类型转换为 Number 的规则
  1. 原始类型

    1. Undefined, NaN

    2. Null, 0

    3. Boolean, true 1, false 0

    4. String, '' 0, 非空非数值 NaN, 非空数值 Number

    5. Number

    6. BigInt

    7. Symbol, 不能转, 会报错

  2. Object

    • 先转换成相应的原始类型值

      • ToPrimitive 会先调用 valueOf() 再调用 toString()

    • 再按原始类型的规则转

// 原始类型
Number(undefined) // NaN
Number(null)      // 0 

Number(true)      // 1
Number(false)     // 0

Number('')        // 0
Number('123')     // 123
Number('hello')   // NaN

Number(Symbol('1'))  // TypeError: Cannot convert a Symbol value to a number

// object
Number([])           // 0
Number({})           // NaN

Number([1])          // 1
Number(['1'])        // 1

Number([1,2,3])      // NaN
Number({name: 'hi'}) // NaN
Number(()=>{})       // NaN
var a = [0];
if ([0]) {
  console.log(a == true);
} else {
  console.log("hi");
}

以上代码的输出是什么?

boolean 和 ==

输出:false

解析:

  • 第 2 行,转 boolean 类型:数组 [0] 不为空,所以值是 true

  • 第 3 行,运算符 ==:如果一个操作数是 boolean,则另一个操作值在比较之前先转换为 Number,而 Number([0]) 的值是 0,于是最终变成了 0 == true

console.log(+true);
console.log(!'hi');

以上代码的输出是什么?

+!

输出:1 false

解析:类型转换

  • 第 1 行,把 true 转成 Number

  • 第 2 行,把 String 转成 Boolean

var a = [1, 2, 3]
var b = [1, 2, 3]
var c = [1, 2, 4]

console.log(a == b)
console.log(a === b)
console.log(a > c)
console.log(a < c)

以上代码的输出是什么?

数组比大小

输出:false, false, false, true

解析:在 JavaScript 中

  • Array 本质上是 Object(引用类型)

    • 引用类型比相等,比的是地址,所以,===== 都不相等

  • 而 Array 的 >< 运算符,方式却类似于字符串,比较字典顺序

    • 所以,a < c 是 true

~~~~~~~~~ 运算符 ~~~~~~~

Object.is()==, === 的区别
  • == 会进行隐式类型转换

  • === 不进行类型转换,是严格相等

  • Object.is() 是在 === 的基础上特别处理了 NaN, -0, +0

Object.is(+0, -0)    // false
Object.is(NaN, NaN)  // true

最重要的是,Object.is() 是有其特殊用途的,而不应该理解为是更严格的相等。

var val = 'hi'
console.log('Value is ' + (val === 'hi') ? 'Something' : 'Nothing')

以上代码的输出是什么?

+? 的优先级

输出:"Something"

解析:+ 的优先级要高于 ?

 const person = {
    firstName: 'Lydia',
    lastname: 'Hallie',
    pet: {
        name: 'Mara',
        breed: 'Dutch Tulip Hound'
    },
    getFullName() {
        return `${this.firstName}/${this.lastName}`
    }
}
console.log(person.pet?.name)
console.log(person.pet?.family?.name)
console.log(person.getFullName?.())
console.log(person.getLastName?.())

以上代码的输出是什么?

?.

输出:Mara undefined Lydia/Hallie undefined

解析:?. 可选操作链,ES2020 新特性。 当 ? 前面的属性不是 undefined 或 null 时才会继续执行后续代码,否则直接返回左侧结果。

~~~~~~~~~ 语句 ~~~~~~~~

function foo(){
    alert(1)
}()
以上代码是 IIFE 吗?为什么?

不是 IIFE。而且代码运行时会报错:Uncaught SyntaxError: Unexpected token ')'

解析:以 function 关键字开头的语句,会被解析为函数声明,而函数声明是不允许直接运行的。只有当解析器把这句话解析为函数表达式时,才能直接运行。比如改成以运算符开头:

!function foo(){
    alert(1)
}();  // true

!function foo(){
    alert(2)
    return 'hi'
}();  // false

(function foo(){
    alert(3)
})(); // undefined, 最日常的写法

(function foo(){
    alert(4)
}()); // undefined, 全部包裹也可以

任何消除函数声明和函数表达式之间歧义的方法,都可以被解析器正确识别。

var i = function(){return 10}();   // undefined
1 && function(){return true}();    // true
1, function(){alert(1)}();         // undefined

如果不在乎返回值,添加一元运算符都是有效的:

!function(){alert(1)}()        // true
+function(){alert(1)}()        // NaN
-function(){alert(1)}()        // NaN
~function(){alert(1)}()        // -1
...

void function(){alert(1)}()    // undefined
new function(){alert(1)}()     // Object {}
delete function(){alert(1)}()  // true

基础概念:语句、表达式、表达式语句

详情可查阅:https://swordair.com/function-and-exclamation-mark/

let obj = {
  name: "Lydia",
  age: 21
}
for (let item in obj) {
  console.log(item)
}

以上代码的输出是什么?

for-in

输出:name, age

解析:for-in 遍历的是对象自身的属性和方法,let key in obj

const myLife = ['吃', '喝', '玩', '乐'];
for (let item in myLife) {
  console.log(item);
}
for (let item of myLife) {
  console.log(item);
}

以上代码的输出是什么?

for-infor-of

输出:

  • 0 1 2 3

  • 吃 喝 玩 乐

解析:

  • for-inlet key in obj

  • for-oflet item in obj

~~~~~~~~~ 方法 ~~~~~~~~

const arr = [1, 2, [3, 4, [5]]];
console.log(arr.flat(1));

以上代码的输出是什么?

Array.flat()

输出:[1, 2, 3, 4, [5]]

扁平化,会创建一个新的、被扁平化的数组。扁平化的深度,取决于参数。

Array.prototype.flat() 会创建一个 new array,用所有的 sub-array elements 递归地 concat 到一起。depth 由参数指定,默认是 1。

const arr1 = [0, 1, 2, [3, 4]];
arr1.flat(1); // [0, 1, 2, 3, 4]

const arr2 = [0, 1, 2, [[[3, 4]]]];
arr2.flat(1); // [0, 1, 2, [[3, 4]]]
arr2.flat(2); // [0, 1, 2, [3, 4]]
arr2.flat(3); // [0, 1, 2, 3, 4]
var ary = Array(3);
ary[0] = 2
ary.map(function(elem) { return '1'; });

以上代码的运行结果是什么?

Array.map()

结果:['1', empty × 2]

解析:Array.prototype.map() 只有在数组中被初始化过的元素,才会被触发

[1, 2, 3, 4].reduce((x, y) => console.log(`x=${x} y=${y}`))

以上代码的输出是什么?

Array.reduce()

输出:

  • x=1 y=2

  • x=undefined y=3

  • x=undefined y=4

解析:最容易理解 reduce() 情况,是求返回数组中所有元素的总和。如下:

// 简写
[1, 2, 3, 4].reduce((x, y) => x + y)

// 增加 log
[1, 2, 3, 4].reduce((x, y) => {
    console.log(x, y)
    return x + y
})

而当回调函数中没有返回值时,则第一个参数为 undefined

const name = 'Lydia Hallie';
const age = 21;

console.log(Number.isNaN(name));
console.log(Number.isNaN(age));

console.log(isNaN(name));
console.log(isNaN(age));

以上代码的输出是什么?

isNaN()Number.isNaN()

输出:false, false, true, false

解析:

  • isNaN() 会先将传入的参数转换为 Number 类型

  • Number.isNaN() 是先判断值的类型是不是 Number,若不是则直接返回 false

    • 即只有当值是 NaN 时才会返回 true

常用的 string 方法

去除字符串首尾空格

使用正则 (^\s*)|(\s*$)

常用的 Array 方法

实现深度拷贝
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;
}
数组去重
  1. ES6 Set 去重 Array.from(new Set(array))

  2. indexOf 循环去重

  3. Object 键值对去重:把数组的值存成 Object 的 key 值,比如 Object[value1] = true

获得对象上的属性

从 ES5 开始,有三种方法可以列出对象的属性

  1. for(let i in obj) 访问对象及其原型链中所有可枚举的类型

  2. object.keys 返回一个数组,包括所有可枚举的属性名称

  3. object.getOwnPropertyNames 返回一个数组包含不可枚举的属性

~~~~~~~~ 原型和类 ~~~~~~~~

  • 原型链的顶端是什么?

  • Object 的原型是什么?

  • Object 的原型的原型是什么?

  • 在数组的原型链上,实现删除数组重复数据的方法

原型链

new 操作符做了哪些事情

new 操作符新建了一个空对象,这个对象原型指向构造函数的 prototype,执行构造函数后返回这个对象。

new 操作符原理

  1. 创建一个类的实例:创建一个空对象 obj,然后把这个空对象的 proto 设置为构造函数的 prototype

  2. 初始化实例:构造函数被传入参数并调用,关键字 this 被设定指向该实例 obj

  3. 返回实例 obj

Function.prototype.a = 1;
Object.prototype.b = 2; 
function A() {}

var a = new A();
console.log(a.a, a.b); 
console.log(A.a, A.b); 

以上代码的输出是什么?

Function 和 Object
  • undefined, 2

  • 1, 2

Function._proto_(getPrototypeOf) 是什么?

获取一个对象的原型:

  • 在 chrome 中,可以用 _proto_

  • 在 ES6 中,可以用 Object.getPrototypeOf

Function.proto 是指 Function 继承自什么对象。

Function.__proto__ === Object.prototype   // false
Function.__proto__ === Function.prototype // true

Function 的原型也是 Function

  1. Hero("37er") 执行结果为 Hi! This is 37er

  2. Hero("37er").kill(1).recover(30) 执行结果为 Hi! This is 37er Kill 1 bug Recover 30 bloods

  3. Hero("37er").sleep(10).kill(2) 执行结果为 Hi! This is 37er 等待 10s 后 Kill 2 bugs

编写代码,满足以上条件
function Hero(name) {
    let o = new Object()
    o.name = name
    console.log(`Hi! This is ${o.name}`)

    o.kill = (num) => {
        console.log(`Kill ${num} bug` + (num > 1 ? 's' : ''))
        return o;
    };

    o.recover = function (num) {
        console.log(`Recover ${num} blood` + (num > 1 ? 's' : ''))
        return o;
    }
    o.sleep = function (s) {
        // TODO. 不能直接连调
        // 只能 Hero("37er").sleep(2).then(o => o.kill(2))
        return new Promise((resolve) => {
            setTimeout(() => {
                console.log('sleep...')
                resolve(o)
            }, s * 1000)
        })
    }
    return o;
}
类的创建和继承
  1. 基于原型链的 new function + prototype

  2. ...

P59

继承的实现方式
  1. 原型链继承,将父类的实例作为子类的原型,他的特点是实例是子类的实例也是父 类的实例,父类新增的原型方法/属性,子类都能够访问,并且原型链继承简单易于实 现,缺点是来自原型对象的所有属性被所有实例共享,无法实现多继承,无法向父类构 造函数传参。

  2. 构造继承,使用父类的构造函数来增强子类实例,即复制父类的实例属性给子类, 构造继承可以向父类传递参数,可以实现多继承,通过 call 多个父类对象。但是构造继 承只能继承父类的实例属性和方法,不能继承原型属性和方法,无法实现函数服用,每 个子类都有父类实例函数的副本,影响性能

  3. 实例继承,为父类实例添加新特性,作为子类实例返回,实例继承的特点是不限制调用方法,不管是 new 子类还是子类返回的对象具有相同的效果,缺点是实例是父类的实例,不是子类的实例,不支持多继承

  4. 拷贝继承

    • 特点:支持多继承

    • 缺点:效率较低,内存占用高(因为要拷贝父类的属性),无法获取父类不可枚举的方法(不可枚举方法,不能使用 for in 访问到)

  5. 组合继承:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

  6. 寄生组合继承:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点

更多 https://www.cnblogs.com/humin/p/4556820.html

两个构造函数,实现 A 继承 B

P108

~~~~~~~~~ 运行时 ~~~~~~~~

作用域类型

let obj1 = {
    name: 'obj1_name',
    print: function(){
        return () => console.log(this.name)
    }
}
let obj2 = { name: 'obj2_name' }

obj1.print()();
obj1.print().call(obj2);
obj1.print.call(obj2)();

以上代码的输出是什么?

this 和箭头函数

输出:obj1_name obj1_name obj2_name

解析:

  • 普通函数,this 指向看执行时的声明

  • 箭头函数,this 值继承父级函数的

    • 箭头函数不绑定自己的 this

bind,apply,call 的区别

改变函数内部 this 指针的指向函数。

通过 apply 和 call,改变函数的 this 指向:

  • 它们两个函数的第一个参数都是一样的,表示要改变指向的那个对象

  • 第二个参数,apply 是数组,而 call 则是 arg1,arg2...这种形式。

// 在另一个对象 A 上,调用 B 对象的方法
B.apply(A, arguments)
B.call(A, args1, args2)

通过 bind 改变 this 作用域会返回一个新的函数,这个函数不会马上执行。

bind 除了返回的是函数之外,其参数和 call 一样。

this 的指向有哪几种?
  1. 默认绑定:全局环境中,this 默认绑定到 window(非严格模式时)

  2. 隐式绑定:一般地,被直接对象所包含的函数调用时,也称为方法调用,this 隐式绑定到该直接对象

  3. 隐式丢失:隐式丢失是指被隐式绑定的函数丢失绑定对象,从而默认绑定到 window

  4. 显式绑定:通过 call(), apply(), bind() 方法把对象绑定到 this 上

  5. new 绑定:如果函数或者方法调用之前带有关键字 new,它就构成构造函数调用。对于 this 绑定来说,称为 new 绑定

    • 构造函数通常不使用 return 关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它会显式返回。在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值

    • 如果构造函数使用 return 语句但没有指定返回值,或者返回一个原始值,那么这时将忽略返回值,同时使用这个新对象作为调用结果

    • 如果构造函数显式地使用 return 语句返回一个对象,那么调用表达式的值就是这个对象

闭包

闭包就是能够读取其它函数内部变量的函数,或者子函数在外调用, 子函数所在的父函数的作用域不会被释放。

TODO.

  • 闭包本身的含义?

  • 在 JS 里,闭包的形式通常是xxx,因为它符合闭包的定义。

  • 在其它语言里,闭包是否有不同的形式?

  • 闭包的使用场景

setTimeout(function(){
    console.log(1)
}, 0);

new Promise(function(resolve,reject){
    console.log(2);
    resolve();
}).then(function(){
    console.log(3)
}).then(function(){
    console.log(4)
});

// Node.js
process.nextTick(function(){
    console.log(5)
});

console.log(6);

以上代码的输出是什么?

Event Loop 中的 Job queue
setTimeout() 的时间含义

setTimeout() 函数只是将事件插入了任务列表,必须等当前代码都执行完,主线程才会去执行它指定的回调函数,有可能要等很久,所以没办法保证回调函数一定会在 setTimeout 指定的时间内执行。

设置的那个时间 ≈ 插入队列的时间 + 等待的时间

如何实现 sleep()
  1. promise

  2. async

  3. generate

  4. while

// 1. promise
function sleep(ms){
    var temple = new Promise((resolve)=>{
            console.log(111);
            setTimeout(resolve, ms) // 起码是异步的
        });
    return temple
}
sleep(500).then(function(){
    console.log(222)
})
// 先输出了 111,延迟 500ms 后输出 222


// 2. async
function sleep(ms){
    return new Promise((resolve)=>
                setTimeout(resolve,ms)
            );
}
async function test(){
    var temple = await sleep(1000);
    console.log(1111)
    return temple
}
test();
// 延迟 1000ms 输出了 1111


// 3. generate
function* sleep(ms){
    yield new Promise(function(resolve,reject){ 
                console.log(111);
                setTimeout(resolve,ms);
            })
}
sleep(500).next().value.then(function(){console.log(2222)})
// 4. while
function sleep(ms){
    var start=Date.now(), expire=start+ms;
    while(Date.now()<expire);
        console.log('1111');
    return;
}
// sleep(1000) 休眠了 1000ms 之后输出了 1111
// 缺点:容易造成死循环(一直占着...主进程...也太不行了吧...)
// 那放在...web worker 里?有点杀鸡用牛刀...
如何解决异步回调地狱?
  • promise

  • async/await

  • generator

promise, generator, async 的使用,P95

promise

promise 及其底层如何实现, P89

async 和 await

generator

让三个 promise 串行执行

P108

实现一个简单的 promise

Promises/A+ https://promisesaplus.com/

实现一个完美符合Promise/A+规范的Promise https://github.com/forthealllight/blog/issues/4

垃圾回收机制

必要性:

  • 由于字符串、对象和数组没有固定大小,所以只有当它们的大小已知时,才能进行动态的存储分配。

  • JavaScript 程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。

不像 C/C++,JS 自己有一套垃圾回收机制(Garbage Collection)。JavaScript 的解释器可以检测到什么时候不再使用一个对象了,一旦确定了一个对象是无用的,它就可以把它所占用的内存释放掉了。

var a = "hello world";
var b = "world";
var a = b;
//这时,会释放掉"hello world",释放内存以便再引用垃圾回收的方法:标记清除、计数引用。

1. 标记清除

这是最常见的垃圾回收方式。

  • 当变量进入环境时,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占的内存,只要执行流程进入相应的环境就可能用到它们。

  • 当离开环境时,就标记为离开环境。

  • 垃圾回收器在运行的时候,会给存储在内存中的变量都加上标记(所有都加),然后去掉环境变量中的变量,以及被环境变量中的变量所引用的变量(条件性去除标记),删除所有被标记的变量,删除的变量无法在环境变量中被访问所以会被删除,最后垃圾回收器完成了内存的清除工作,并回收它们所占用的内存。

2. 引用记数法

记录每个值被引用的次数。

  • 当声明了一个变量,并用一个引用类型的值赋值给改变量,则这个值的引用次数为 1

  • 相反的,如果包含了对这个值引用的变量又取得了另外一个值,则原先的引用值引用次 数就减 1,

  • 当这个值的引用次数为 0 的时候,说明没有办法再访问这个值了,因此就把 所占的内存给回收进来,这样垃圾收集器再次运行的时候,就会释放引用次数为 0 的这些值。

用引用计数法会存在内存泄露。比如:

function problem() {
    var objA = new Object();
    var objB = new Object();
    objA.someOtherObject = objB;
    objB.anotherObject = objA;
}
// objA 和 objB 通过各自的属性相互引用,此时,两个对象的引用次数都为 2
// 在采用引用计数的策略中,由于函数执行之后,这两个对象都离开了作用域,
// 函数执行完成之后,因为计数不为 0,这样的相互引用如果大量存在就会导致内存泄露
// 在 DOM 对象中,也容易存在这种问题
var element = document.getElementById(’‘);
var myObj = new Object();
myObj.element=element;
element.someObject=myObj;
// 这样就不会有垃圾回收的过程

~~~~~~~~~ 其它 ~~~~~~~~~

前端模块化(CommonJS,AMD,ES Modules)

将复杂的文件拆成一个个独立的模块(比如单个 JS 文件),这样有利于重用(复用性)和维护(版本迭代)。一个模块是能实现特定功能的文件,有了模块就可以方便的使用别人的代码,想要什么功能就能加载什么模块。

与此同时,也会引来模块之间相互依赖的问题,所以有了 commonJS 规范,AMD,CMD 规范等等,以及用于 JS 打包(编译等处理)的 工具 webpack。

  1. CommonJS 是一种模块规范,最初被用在 Node.js,成为 Node.js 的模块规范

  2. 而浏览器端,在 ES6 之前,类似的模块规范有 AMD

  3. ES6 Module 是在语言标准的层面上实现了模块功能。有望成为浏览器和服务器通用的模块解决方案

CommonJS vs AMD

CommonJS 开始于服务器端的模块化,同步定义的模块化,每个模块都是一个单独的作用域。

  • 模块输出 modules.exports

  • 模块加载 require() 引入模块

AMD,异步模块定义。

requireJS 实现了 AMD 规范,主要用于解决下述两个问题:

  1. 多个文件有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器

  2. 加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应的时间越长

// requireJS 定义了一个函数 define,它是全局变量
// 定义模块 define()
define(['dependency'], function(){ 
        var name = 'Byron';
        function printName(){ console.log(name);
    }
    return {
        printName: printName
    };
});

// 加载模块 require(),异步加载,这样不会阻塞浏览器
require(['myModule'], function (my){
    my.printName();
}
// 当前面的模块加载成功,回调才会被执行。

CommonJS vs ES Module

使用上的差别,主要有:

  1. 模块输出:值的拷贝 vs 值的引用

  2. 时机:运行时加载 vs 编译时输出接口

  3. 形式:单个值导出 vs 可导出多个

  4. 动态语法可以写在判断里 vs 静态语法只能写在顶层

  5. this 值:当前模块 vs undefined(默认是严格模式)

"use strict";

ES5 引入了严格模式,旨在让代码更合理、更安全。

它对 JavaScript 的使用添加了一些限制,比如:

  • 禁止使用 with 语句

  • 禁止 this 指向全局对象 (???)

  • 对象的属性名不能重复

此外,针对严格模式,编译器也会有特定的优化,所以代码的运行速度也会快一些。

严格模式,还可以很好地向后兼容,为未来的 JavaScript 做好铺垫。

eval

功能:将对应的字符串解析成 JS 并执行

应该避免使用,因为非常消耗性能(2 次,一次解析成 JS,一次执行)

ES6,ES7 的新语法
  1. 变量的声明和定义

    • 增加了 let、const 声明变量

    • 它们是块级作用域的(ES5 只有全局作用域和函数作用域)

    • 块级作用域的好处是不再需要立即执行的函数表达式,循环体中的闭包不再有问题

  2. 箭头函数

  3. 异步的 promise, generator, async/await

  4. 模块化,更方便地进行模块化编程

  5. class,可以更好的面向对象编程

其它:

  1. 数据类型

    • 新的原始类型 symbol

    • 新的数据结构 set 和 map

  2. 对已有类型的对象包装器,新增了一些方法。比如

    • 字符串:模板字符串

    • 函数:默认参数

    • 对象属性:?.

  3. 运算符

    • 结构赋值

    • rest

  • JS 的语言特性

  • C++, Java, JavaScript 这三种语言的区别

Last updated