乐趣区

JavaScript基础篇

把知识串一串,连成线,形成体系,从此走上大神之路啦,道路可能会曲折一点,但是咸鱼也要翻一翻身撒~

一、变量提升

何为变量提升?

在 JavaScript 中,函数及变量的声明都将被提升到函数的最顶部 (函数声明的优先级高于变量声明的优先级)

这样就造成了一种不同于其他语言的现象,初看甚至觉得有些诡异:变量可以先使用再声明。举个栗子:

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

// 输出为 Goodbye Jack

为什么会出现这样情况呢?

在 JavaScript 中,变量声明与赋值的分离,如 var a = 2 这个代码是分两步进行的,编译阶段之行变量声明 var a,在执行阶段进行赋值 a = 2,于是便造成了了变量声明提前情况的发生。

解析:对于第二个例子,由于存在变量提升,所以变量声明先于 if 判断,所以此时 name = undefined,于是便输出了 Goodbye Jack

二、隐式转换

前段时间,前端各大博客被一道题刷屏了

++[[]][+[]]+[+[]]==10?

这道题怎么去解决呢,这就涉及到了 JS 的隐式转换相关的知识了。

简述隐式转换规则

对于原始类型:Undefined、Null、Boolean、Number、String

1,加号运算符(+):若后面的是数字,会直接相加得出结果,如 1 + 1 = 2;若后面的是字符类型,则会进行字符拼接,如 1 + ‘1’ = ’11’。
2,减号运算符(-):若后面的是数字,会直接相减得出结果;若后面的字符,则会将其转为数字类型,然后相减得出结果。
3,== 运算负责:

  • undefined == null,结果为 true
  • String == Boolean,需要将两个操作数同时转化为 Number
  • String/Boolean == Number,需要将 String/Boolean 转为 Number

对于对象类型:Object
当对象与一个非对象进行比较等操作时,需要先将其转化为原始类型:首先调用 valueOf(),若结果是原始类型,则返回结果;若结果不是原始类型,则继续调用 toSring(),返回其结果,若结果依然不是原始类型,则会抛出一个类型错误。

这里有一道很火的面试题,就是利用对象的类型转换原理:

a == 1 && a == 2 && a == 3

// 答案:var a = {num : 0};
a.valueOf = function() {return ++a.num;}

以上大概为基础的隐式转换规则,可能不太完善,欢迎大家留言补充。好,有了这些准备后,让我们再来看下一开始的题目,让我们来逐步拆解:

1,根据运算符的优先级,我们可以得到:(++[[]][+[]])+[+[]]
2,根据隐式转换,我们得到:(++[[]][0])+[0]
3,再次简化:(++[]) + [0]
4,这个时候就很明朗了,最终划为字符拼接 '1' + '0' = '10';

三,闭包

什么是闭包?

简单的讲,闭包就是指有权访问另一个函数作用域中的变量的函数。

MDN 上面这么说:闭包是一种特殊的对象,是函数和声明该函数的词法环境的组合。

产生一个闭包

function func() {
    var a = 1;
    return function fn() {console.log(a);
    }
}

func()();   // 1

这里函数 func 在调用后,其作用域并没有被销毁,依然可以被函数 fn 访问,所以输出为 1。
这里有道很经典的面试题

function fun(n,o){console.log(o);
  return {fun: function(m){return fun(m,n);
    }
  };
}

var a = fun(0);  // ?
a.fun(1);        // ?        
a.fun(2);        // ?
a.fun(3);        // ?

var b = fun(0).fun(1).fun(2).fun(3);  // ?

var c = fun(0).fun(1);  // ?
c.fun(2);        // ?
c.fun(3);        // ?

undefined
0
0
0
undefined, 0, 1, 2
undefined, 0
1
1

哈哈,有点绕,有兴趣的同学可以简单看下。

四,深、浅克隆

在实际开发或面试中,我们经常会碰到克隆的问题,这里我们简单的总结下。

浅克隆

浅克隆就是复制对象的引用,复制后的对象指向的都是同一个对象的引用,彼此之间的操作会互相影响

var a = [1,2,3];
var b = a;
b[3] = 4;
console.log(a, b);

// [1,2,3,4] [1,2,3,4]

实际开发中,若需要同步对象的变化,往往用的就是浅克隆,直接复制对象引用即可。

深克隆

开发过程中,我们往往需要断开对象引用,不影响原对象,这个时候我们就用到深克隆了,有如下方法:

方法一

JSON.parse(JSON.stringify()),对于大多数情况都可以用这种方法解决,一步到位。但是若对象中存在正则表达式类型、函数类型等的话,会出现问题:会直接丢失相应的值,同时如果对象中存在循环引用的情况也无法正确处理

let a = {name: '小明'};
let b = JSON.parse(JSON.stringify(a));
b.age = 18;
console.log(a, b);

// {name: '小明'} {name: "小明", age: 18}

方法二

对于数组,我们可以利用 Array 的 slice 和 concat 方法来实现深克隆

let a = [1,2,3];
let b = a.slice();
b.push(4);
console.log(a, b);

// [1,2,3]  [1,2,3,4]
let a1 = [1,2,3];
let b1 = a.concat(4);
b1.push(5);
console.log(a, b);
// [1,2,3]  [1,2,3,4,5]

方法三

jQuery 中的 extend 复制方法:$.extend(true, target, obj)

let a = {name: '小明'};
let b = {}
$.extend(true, b, a);
b.age = 18;
console.log(a, b);

// {name: "小明"} {name: "小明", age: 18}

五、this 指向

关于 this 指向的问题,这里是有一定判断方法的:

位置:this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里调用
规则:默认绑定、隐式绑定、显式绑定、new 绑定

我们在实际判断的时候,需要将二者结合起来。

1,默认规则

var name = '小明';
function print() {console.log(this.name);  // '小明'
    console.log(this);   //window 对象
}
print(); 
// '小明'

解析:print()直接使用不带任何修饰的函数引用进行的调用,这个时候只能使用默认绑定规则,即 this 指向全局对象,所以此题输出为:’ 小明 ’

2,隐式绑定

function foo() {console.log(this.a)
}

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

解析:当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。所以此题的 this 被绑定到 obj,于是 this.a 和 obj.a 是一样的。

这里有两点点需要注意:
1,对象属性引用链中只有上一层或者说最后一层在调用位置中起作用,举例如下:

function foo() {console.log(this.a);
}

var obj2 = {
    a: 10,
    foo: foo
}

var obj1 = {
    a: 2,
    obj2: obj2
}

obj1.obj2.foo();  // 10

2,隐式丢失:被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上。举例如下:

var a = 'hello world';

function foo(){console.log(this.a)
}

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

var print = obj.foo;
print();    // hello world

解析:虽然 print 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,所以此时 print()其实是一个不带任何修饰的函数调用,应用了隐式绑定。

3,显示绑定

利用 call(),apply(),bind()强制绑定 this 指向的我们称之为显示绑定,举例如下:

function foo() {console.log(this.a);
}

var obj = {a:1}

foo.call(obj);  // 1

这里有一点需要注意:显示绑定依然无法解决上面提到的丢失绑定问题。举例如下:

var a = 'hello world';

function foo(){console.log(this.a)
}

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

var print = obj.foo;
print.bind(obj)
print();    // hello world

这里有关 call、apply、bind 的具体用法就不再一一阐述了,后面的部分会详细讲解。

4,new 绑定

这是最后一条 this 的绑定规则,使用 new 来调用函数,或者说发生构造函数调用时,会执行下面的操作:

  • 创建 (或者说构造) 一个全新的对象
  • 这个新对象会被执行 [[Prototype]] 连接
  • 这个新对象会绑定到函数调用的 this
  • 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。

这个过程中发生了 this 绑定,举例如下:

function Person(name) {this.name = name;}
var p = new Person('小明');
console.log(p.name);    // 小明

5,优先级

这里不再一一举例对比优先级,直接给出结论:new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定,有兴趣的同学可以实际比对一下。
常规 this 指向判断流程:

  • 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的就是新创建的对象
  • 函数是否通过 call、apply、bind(显示绑定) ? 如果是的话,this 绑定的是指定的对象
  • 函数是否在某个上下文对象中被调用(隐时绑定) ? 如果是的话,this 绑定的是那个上下文对象
  • 如果都不是的话,使用默认绑定

六、call、apply、bind

1,call()

定义:

使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数

语法:

fun.call(thisArg, arg1, arg2, …)

参数:

thisArg:(1) 不传,或者传 null,undefined,函数中的 this 指向 window 对象
(2) 传递另一个函数的函数名,函数中的 this 指向这个函数的引用,并不一定是该函数执行时真正的 this 值
(3) 值为原始值(数字,字符串,布尔值) 的 this 会指向该原始值的自动包装对象,如 String、Number、Boolean(4)传递一个对象,函数中的 this 指向这个对象

arg1, arg2, …:指定的参数列表

举例如下:

var obj = {a: '小明'};

function print() {console.log(this);
}

print.call(obj);  // {a: '小明'}

实现 call 方法:

Function.prototype.selfCall = function(context, ...args) {
    let fn = this;
    context || (context = window);
    if (typeof fn !== 'function') throw new TypeError('this is not function');
    let caller = Symbol('caller');
    context[caller] = fn;
    let res = context[caller](...args);
    delete context[caller];
    return res;
}

2,apply()

apply()方法与 call()方法相似,区别在于:call 方法接受的是若干个参数列表,而 apply 接收的是一个包含多个参数的数组。

举例如下:

var arr = [34,5,3,6,54,6,-67,5,7,6,-8,687];

Math.max.apply(Math, arr);  // 687
Math.min.call(Math, ...arr);   // -67

3,bind()

定义:

bind()方法创建一个新的函数,在调用时设置 this 关键字为提供的值。并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。

语法:

function.bind(thisArg[, arg1[, arg2[, …]]])

参数:

thisArg:调用绑定函数时作为 this 参数传递给目标函数的值
arg1, arg2, …:当目标函数被调用时,预先添加到绑定函数的参数列表中的参数。

举例如下:

function print() {console.log(this);
}

let obj = {name: '小明'};
let fn = print.bind(obj);

fn();    // {name: "小明"}

七、Promise

1,什么是 Promise?

Promise 是 JS 异步编程中的重要概念,异步抽象处理对象,是目前比较流行 Javascript 异步编程解决方案之一

2,创建 Promise

方法一:new Promise

// 声明 Promise 后会立即执行
var promise = new Promise(function(resolve, reject) {resolve('Hello');
})
console.log(promise);   // Promise{<resolved>: "Hello"}

方法二:直接创建

var promise = Promise.resolve('Hello');
console.log(promise);   // Promise{<resolved>: "Hello"}

3,Promise 状态

promise 相当于一个状态机,具有三种状态:

  • pending
  • fulfilled
  • rejected

(1) promise 对象初始化状态为 pending

(2) 当调用 resolve(成功),会由 pending => fulfilled

(3) 当调用 reject(失败),会由 pending => rejected

注:promsie 状态 只能由 pending => fulfilled/rejected, 一旦修改就不能再变

4,Promise API

1,Promise.prototype.then()

then() 方法返回一个 Promise。它最多需要有两个参数:Promise 的成功 (onFulfilled) 和 失败情况 (onRejected) 的回调函数。

举例如下:

var promise = new Promise((resolve, reject) => {
    // 成功
    resolve('hello');
});

promise.then((res) => {console.log(res);    // hello
    return Promise.reject('error');
}).then((success) => {console.log('success', success);
}, (err) => {console.log('error', err);    // error error
});

2,Promise.resolve()

Promise.resolve(value)方法返回一个以给定值解析后的 Promise 对象。但如果这个值是个 thenable(即带有 then 方法),返回的 promise 会“跟随”这个 thenable 的对象,采用它的最终状态(指 resolved/rejected/pending/settled);如果传入的 value 本身就是 promise 对象,则该对象作为 Promise.resolve 方法的返回值返回;否则以该值为成功状态返回 promise 对象。

举例如下:

var promise = Promise.resolve('hello');

promise.then((res) => {console.log(res);
});

// hello
// Promise {<resolved>: undefined}

此时 promise 的状态为题 fulfilled

3,Promise.reject()

Promise.reject(reason)方法返回一个带有拒绝原因 reason 参数的 Promise 对象。

举例如下:

var promise = Promise.reject('error');

promise.then((res) => {console.log('success', res);
}, (res) => {console.log('error', res);    // error error
});

4,Promise.race()

Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。

举例如下:

var promise1 = new Promise(function(resolve, reject) {setTimeout(resolve, 500, 'one');
});

var promise2 = new Promise(function(resolve, reject) {setTimeout(resolve, 100, 'two');
});

Promise.race([promise1, promise2]).then(function(value) {console.log(value);    // two
});

5,Promise.all()

Promise.all(iterable) 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败原因的是第一个失败 promise 的结果。

举例如下:

var promise1 = Promise.resolve(3);
var promise2 = 42;
var promise3 = new Promise(function(resolve, reject) {setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3]).then(function(values) {console.log(values);    // [3, 42, "foo"]
});

6,Promise.prototype.finally()

finally() 方法返回一个 Promise。在 promise 结束时,无论结果是 fulfilled 或者是 rejected,都会执行指定的回调函数。这为在 Promise 是否成功完成后都需要执行的代码提供了一种方式。
这避免了同样的语句需要在 then()和 catch()中各写一次的情况。

举例如下:

var promise = Promise.resolve('Hello');
promise.then((res) => {console.log(res);   // Hello
}).finally((res) => {console.log('finally');     // finally
}) 

7,Promise.prototype.catch()

catch() 方法返回一个 Promise,并且处理拒绝的情况,捕获前面 then 中发送的异常

只要 Promsie 状态更改为 reject 或者抛出异常,都会进入 catch 方法。举例如下:

var promise1 = Promise.reject('Hello');
promise1.then((res) => {console.log('success' + res);
}).catch((res) => {console.log('catch' + res);     // catch Hello
})

八、Event Loop

1,前言

Event Loop 即事件循环,是指浏览器或 Node 的一种解决 javaScript 单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。

2,宏任务与微任务

在 JavaScript 中,任务被分为两种,一种宏任务(MacroTask)也叫 Task,一种叫微任务(MicroTask)。

宏任务:

  • script 全部代码
  • setTimeout
  • setInterval
  • setImmediate (Node 独有)
  • I/O
  • UI rendering (浏览器独有)

微任务:

  • process.nextTick (Node 独有)
  • Promise
  • Object.observe
  • MutationObserver

3,浏览器的 Event Loop

浏览器中的事件循环机制是什么样子呢?不废话,直接上图:

过程如下:

  • 执行全局 Script 同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如 setTimeout 等);
  • 全局 Script 代码执行完毕后,调用栈 Stack 会清空;
  • 检查微任务队列是否为空,若不为空,则取出位于队首的回调任务,放入调用栈 Stack 中执行,队列长度减 1。如此循环往复,直至微任务队列为空
  • 微任务队列为空后,检查宏任务队列是否为空,若不为空,则取出宏队列中位于队首的任务,放入 Stack 中执行,队列长度减 1。如此循环往复,直至宏任务队列为空

举例如下:

console.log('script start');

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

Promise.resolve().then(function() {console.log('promise1');
}).then(function() {console.log('promise2');
});
console.log('script end');

答案如下:

script start、script end、promise1、promise2、setTimeout

解析:
step1

console.log('script start');

Stack Queue: [console]

Macrotask Queue: []

Microtask Queue: []

打印结果:1

step2

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

setTimeout 属于宏任务,所以:

Stack Queue: [setTimeout]

Macrotask Queue: [callback1]

Microtask Queue: []

step3

Promise.resolve().then(function() {console.log('promise1');
}).then(function() {console.log('promise2');
});

promise 属于微任务,所以有:
Stack Queue: [promise]

Macrotask Queue: [callback1]

Microtask Queue: [callback2]

step4

console.log('script end');

同步任务,直接执行

打印结果:script end

step5
遍历微任务队列:Microtask Queue: [callback2],执行其函数

打印顺序依次为:promise1、promise2

step6
微任务队列为空后,遍历宏任务队列:Macrotask Queue: [callback1],执行其回调函数

打印结果:setTimeout

所以最终结果为:script start、script end、promise1、promise2、setTimeout

九、总结

由于时间比较仓促,本次总结还存在着许多遗漏,如 JS 原型,node 环境下的 Event Loop,函数柯里化等,也有许多理解不到位的情况,日后会逐渐完善与补充。

注:如果文章中有不准确的地方,欢迎大家留言交流。????
作者:易企秀——易小星
退出移动版