0%

前端知识体系梳理(二-2)—— 重新认识JS

上篇聊的HTML,CSS 这篇聊聊JS

提纲:

  • 变量类型
    • JS的类型
    • 类型识别
  • 原型、原型链
    • 原型及原型链
    • 类继承及原型继承
  • 作用域和闭包
    • this、变量提升、作用域链
    • 闭包的概念及应用
  • 异步
    • 同步 VS 异步
    • 异步和单线程
    • 前端异步的场景
  • ES6/7新标准
    • 箭头函数
    • Module
    • Class
    • Set & Map
    • Promise

变量类型

JS的类型

JavaScript 是一种弱类型脚本语言,所谓弱类型指的是定义变量时,不需要什么类型,在程序运行过程中会自动判断类型。

ECMAScript 中定义了 6 种原始类型(原始类型不包含 Object):

  • Boolean
  • String
  • Number
  • Null
  • Undefined
  • Symbol(ES6 新定义)

根据 JavaScript 中的变量类型传递方式,又分为值类型和引用类型。在参数传递方式上,值类型是按值传递,引用类型是按共享传递。

JS 中这种设计的原因是:按值传递的类型,复制一份存入栈内存,这类类型一般不占用太多内存,而且按值传递保证了其访问速度。按共享传递的类型,是复制其引用,而不是整个复制其值(C 语言中的指针),保证过大的对象等不会因为不停复制内容而造成内存的浪费。

类型识别

一表在手,天下我有
【===】Null Undefined
【typeof】 Object Boolean Number String Symbol
【toString】 Object Array Date RegExp Function …
【instanceof】 MyClassA MyClassB MyClassC …

undefined、null关系
undefined === undefined // true
null === null // true
undefined == null // true
undefined === null // false

typeof例外
typeof null === ‘object’
typeof Function || function(){} === ‘function’

es6新增:
typeof Symbol() // ‘symbol’

toString封装:除了自定义类型,都可以识别
function type( param ) {
return Object.prototype.toString.call(param).slice(8, -1).toLowerCase();
}
type({}) // ‘object’

instanceof: 多用于判断对象是否是构造函数的实例,可识别自定义对象类型

原型、原型链

原型及原型链

  • 实例有_proto_,构造器有prototype
  • 如果构造器又是实例就既有prototype又有_proto_
  • _proto_属性不可直接访问,prototype属性可直接访问

原型共享属性方法。属性查找在原型链上;属性增删改都在实例对象上,不论原型链上有无属性(构造器可以改变原型上的属性)。
查找属性的链就是 原型链,最上层 —— Object.prototype.proto === null

hasOwnProperty判断是否是实例对象自身属性。

1
2
3
4
5
6
7
var item
for (item in f) {
// 高级浏览器已经在 for in 中屏蔽了来自原型的属性,但是这里建议大家还是加上这个判断,保证程序的健壮性
if (f.hasOwnProperty(item)) {
console.log(item)
}
}

类继承及原型继承

类继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(function() {
function ClassA() {};
ClassA.classMethod = function() {};
ClassA.prototype.api = function() {};

function ClassB() {
ClassA.apply(this, arguments);
}
ClassB.prototype = new ClassA(); // 这里不能使用ClassA.prototype不然ClassA、ClassB共享ClassA的prototype了,修改一个互相影响。这里有一个问题就是ClassB的原型上会冗余ClassA构造器上的属性,所以可以改用下边的原型继承--ClassB.prototype = Object.create(ClassA.prototype)。这种方式之后constructor没有,要手动加上。
ClassB.prototype.constructor = ClassB;
ClassB.prototype.api = function() {
ClassA.prototype.api.apply(this, arguments);
}

var b = new ClassB();
b.api();
})();

原型继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(function() {
var proto = {
action1: function() {
},
action2: function() {
}
}
var obj = Object.create(proto); // 让proto作为obj的原型
})()
---------------------
// 模拟实现Object.create
var clone = (function(){
var F = function() {};
return function(proto) {
F.prototype = proto;
return new F();
}
})();

Object.create(proto)创建的原型没有自己的属性和方法,只有一个原型对象引用_proto_
使用new的构造函数调用会生成.prototype和.constructor引用 ,而使用create方法生成的关联关系不会。
Object.create(null)创建的对象没有prototype _proto_,所以instanceof 总会返回false。
这些对象通常被称作“字典”,其完全不会受到原型链的干扰,因此非常适合用来存储数据。

作用域和闭包

this、变量提升、作用域链

this指向有哪些

全局环境中:this指向全局对象(window)
构造函数:this指向新创建的对象
函数调用:this指向函数的调用者
new Function: this指向全局对象(window)
eval: this为上下文中的this
箭头函数:this为上下文中的this
setTimeout, map中函数:this指向全局对象(window)
apply、call、bind第一个参数为null:函数中this指向全局对象(window)

1
2
3
4
5
6
7
function Car() {
}
Car.a = 3;
Car.test = function() { console.log( 'this是:',this.a) }
var bar = Car.test;
Car.test(); // this是:3
bar() // this是:undefined

改变this指向

下边3个方法改变a方法中的this指向到b,都是【函数借用】— b调用函数a,函数a中this指向b
apply: a.apply(b, [arg1, arg2]) //b调用a,this为b,只2个参数,第2个为数组 (返回的为数值)
call: a.call(b, arg1, arg2) // b调用a,this为b,n个参数 (返回的为数值)
bind:var o = a.bind(b, arg1, arg2) // bind实现延时调用( 返回的为函数 )

延伸
分解数组:
es5:

1
2
3
4
5
6
var arr = [1,2,3];
function test(a, b, c) {
console.log(this); // Window
console.log('参数分别为:', a, b, c); // 参数分别为: 1 2 3
}
test.apply(null, arr); // 第一个参数为null,上边this为window对象

es6:

1
2
3
4
5
6
var arr = [1,2,3];
function test(...values) { // rest参数
console.log('values为:', values); // values为: [1, 2, 3]
console.log(Object.prototype.toString.call(values).slice(8, -1).toLowerCase()); // array — call的使用
}
test(...arr); // 扩展运算符

变量提升

同一作用域中变量、参数、函数同名时提升的优先级:函数 > 参数 > 变量
当然提升完之后还有执行的步骤。不声明直接使用变量会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function test(a){
var a;
function a() {

}
console.log(a);
}
test(2) // 结果为函数
---------------------------------
function test(a){
var a;
console.log(a);
}
test(2) // 结果为2
---------------------------------
function test(a){
var a = 1;
function a() {

}
console.log(a);
}
test(2) // 结果为1
1
2
3
4
5
6
7
8
9
10
11
12
13
console.log(a)  // undefined
var a = 100

fn('zhangsan') // 'zhangsan' 20
function fn(name) {
age = 20
console.log(name, age)
var age
}

console.log(b); // 这里报错
// Uncaught ReferenceError: b is not defined
b = 100;

作用域链

查找当前作用域没有的变量时会一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是 作用域链 。

闭包的概念及应用

概念

闭包大概是这样的:内部函数使用了外部函数的变量,外部函数已退出,内部函数可访问。
例子:
实现a(2)(3) === 5;

1
2
3
4
5
6
function a( m ) {
return function( n ) {
return m + n;
}
}
a(2)(3); // 5

实现自增:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function a() {
var num = 0;
a = function() {
return ++num;
}
return ++num;
}
a() // 1
a() // 2
...
--------------------------
var a = (function(){
var num = 0;
return function() {
return ++num;
}
})();
a() // 1
a() // 2
...

使用场景

保存变量现场

1
2
3
4
5
6
7
8
9
var addHandlers = function( nodes ) {
for( var i = 0; i < nodes.length; i++ ) {
nodes[i].addEventListener('click', (function(i){
return function() {
alert(i);
}
})(i), true); // 第3个的参数: 默认dom事件里边处理的是冒泡过程,此参数为true时处理的是捕获过程。
}
}

封装:自由变量observerList在外边是不能被访问的,而在返回中可以被访问,这就是封装的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var observer  = (function() {
var observerList = [];
return {
add: function(obj) {
observerList.push(obj);
},
empty: function() {
observerList = [];
},
getCount: function() {
return observerList.length;
},
get: function() {
return observerList;
}
}
})();

提炼
函数嵌套形式使用场景:外层传函数,内层传参数
表达式形式使用场景:只需要内层穿参数 或者 麻烦点内层函数参数都传(splice argument获取第一个参数做函数,剩下的做参数)。

函数嵌套形式 外层函数中保留变量的空间是独立的
表达式形式 外层函数中保留变量的空间是共享的1个

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
// 函数嵌套形式。其中res1 、res2 保留变量空间是隔离的。
function _once(fn) {
var result, isFire = false;
return function() {

if(isFire) {
return result;
}
isFire = true;
result = fn.apply(this, arguments)
return result;
}
}
function test(a, b) {
return a + b;
}
function test1( c ) {
return c + 'tys';
}
var res1 = _once(test);
res1(1,2) // 3
res1(1,3) // 3

var res2 = _once(test1);
res2(1) // "1tys"
res2(2) // "1tys"

res1(2,4) // 3

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
// 表达式形式。共享1个保留变量的空间,除非再用表达式赋值一次。
var _once = (function() {
var result, fun = false;
return function() {
fn = Array.prototype.shift.call(arguments);
if(fun === fn) {
return result
}
fun = fn;
result = fn.apply(this, arguments);
return result;
}
})();
function test(a, b) {
return a + b;
}
function test1( c ) {
return c + 'tys';
}

_once(test, 1,2) // 3
_once(test, 1,3) // 3
_once(test1, 3) // "3tys"
_once(test1, 4) // "3tys"
_once(test,1, 4) // 5
_once(test,1, 5) // 5

异步

前端异步的场景

定时 setTimeout setInverval(第一篇有详尽分析,移步过去看)
网络请求,如 Ajax <img>加载

img 代码示例(常用于打点统计)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
console.log('start')
var img = document.createElement('img')
// 或者 img = new Image()
img.onload = function () {
console.log('loaded')
img.onload = null
}
img.src = '//www.lgstatic.com/www/static/common/widgets/header_c/modules/img/logo@2x_520eb33.png'
console.log('end')

结果:
start
end
loaded

ES6/7新标准

箭头函数

可以解决 ES6 之前函数执行中this是全局变量的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function fn() {
console.log('real', this) // {a: 100} ,该作用域下的 this 的真实的值
var arr = [1, 2, 3]
// 普通 JS
arr.map(function (item) {
console.log('js', this) // window 。普通函数,这里打印出来的是全局变量,令人费解
return item + 1
})
// 箭头函数
arr.map(item => {
console.log('es6', this) // {a: 100} 。箭头函数,这里打印的就是父作用域的 this
return item + 1
})
}
fn.call({a: 100})

Module

如果只是输出一个唯一的对象,使用export default即可:

1
2
3
4
5
6
7
8
// 创建 util1.js 文件,内容如
export default {
a: 100
}

// 创建 index.js 文件,内容如
import obj from './util1.js'
console.log(obj)

如果想要输出许多个对象,就不能用default了,且import时候要加{}:

1
2
3
4
5
6
7
8
9
10
11
12
// 创建 util2.js 文件,内容如
export function fn1() {
alert('fn1')
}
export function fn2() {
alert('fn2')
}

// 创建 index.js 文件,内容如
import { fn1, fn2 } from './util2.js'
fn1()
fn2()

Class

class 其实一直是 JS 的关键字(保留字),ES6才使用 。 ES6 的 class 就是取代之前构造函数初始化对象的形式,从语法上更加符合面向对象的写法。例如:
JS 构造函数的写法

1
2
3
4
5
6
7
8
9
10
11
function MathHandle(x, y) {
this.x = x;
this.y = y;
}

MathHandle.prototype.add = function () {
return this.x + this.y;
};

var m = new MathHandle(1, 2);
console.log(m.add())

用 ES6 class 的写法

1
2
3
4
5
6
7
8
9
10
11
12
class MathHandle { // class语法形式:class Name {...}
constructor(x, y) { // 构造器,对应构造函数函数体内容,初始化实例时默认执行
this.x = x;
this.y = y;
}

add() { // 函数并没有function关键字
return this.x + this.y;
}
}
const m = new MathHandle(1, 2);
console.log(m.add())

JS 构造函数实现继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 动物
function Animal() {
this.eat = function () {
console.log('animal eat')
}
}
// 狗
function Dog() {
this.bark = function () {
console.log('dog bark')
}
}
Dog.prototype = new Animal()
// 哈士奇
var hashiqi = new Dog()

ES6 class实现继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Animal {
constructor(name) {
this.name = name
}
eat() {
console.log(`${this.name} eat`)
}
}

class Dog extends Animal { // extends即可实现继承,更加符合经典的写法
constructor(name) {
super(name) // 调用父类的constructor
this.name = name
}
say() {
console.log(`${this.name} say`)
}
}
const dog = new Dog('哈士奇')
dog.say()
dog.eat()

Set & Map

  • Set 类似于数组,但数组可以允许元素重复,Set 不允许元素重复
  • Map 类似于对象,但普通对象的 key 必须是字符串或者数字,而 Map 的 key 可以是任何数据类型

Set:

1
2
const set = new Set([1, 2, 3, 4, 4]);
console.log(set) // Set(4) {1, 2, 3, 4}

Set 实例的属性和方法有:

  • size:获取元素数量。
  • add(value):添加元素,返回 Set 实例本身。
  • delete(value):删除元素,返回一个布尔值,表示删除是否成功。
  • has(value):返回一个布尔值,表示该值是否是 Set 实例的元素。
  • clear():清除所有元素,没有返回值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const s = new Set();
s.add(1).add(2).add(2); // 添加元素

s.size // 2

s.has(1) // true
s.has(2) // true
s.has(3) // false

s.delete(2);
s.has(2) // false

s.clear();
console.log(s); // Set(0) {}

Set 实例的遍历,可使用如下方法:

  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。不过由于 Set结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys()和values()返回结果一致。
  • entries():返回键值对的遍历器。
  • forEach():使用回调函数遍历每个成员。
    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
    let set = new Set(['aaa', 'bbb', 'ccc']);

    for (let item of set.keys()) {
    console.log(item);
    }
    // aaa
    // bbb
    // ccc

    for (let item of set.values()) {
    console.log(item);
    }
    // aaa
    // bbb
    // ccc

    for (let item of set.entries()) {
    console.log(item);
    }
    // ["aaa", "aaa"]
    // ["bbb", "bbb"]
    // ["ccc", "ccc"]

    set.forEach((value, key) => console.log(key + ' : ' + value))
    // aaa : aaa
    // bbb : bbb
    // ccc : ccc

Map:
Map 的用法和普通对象基本一致,先看一下它能用非字符串或者数字作为 key 的特性。

1
2
3
4
5
6
7
8
9
const map = new Map();
const obj = {p: 'Hello World'};

map.set(obj, 'OK')
map.get(obj) // "OK"

map.has(obj) // true
map.delete(obj) // true
map.has(obj) // false

Map 实例的属性和方法如下

  • size:获取成员的数量

  • set:设置成员 key 和 value

  • get:获取成员属性值

  • has:判断成员是否存在

  • delete:删除成员

  • clear:清空所有

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const map = new Map();
    map.set('aaa', 100);
    map.set('bbb', 200);

    map.size // 2

    map.get('aaa') // 100

    map.has('aaa') // true

    map.delete('aaa')
    map.has('aaa') // false

    map.clear()

    Map 实例的遍历方法有

  • keys():返回键名的遍历器。

  • values():返回键值的遍历器。

  • entries():返回所有成员的遍历器。

  • forEach():遍历 Map 的所有成员。

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
const map = new Map();
map.set('aaa', 100);
map.set('bbb', 200);

for (let key of map.keys()) {
console.log(key);
}
// "aaa"
// "bbb"

for (let value of map.values()) {
console.log(value);
}
// 100
// 200

for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// aaa 100
// bbb 200

// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// aaa 100
// bbb 200

Promise

Promise是 CommonJS 提出来的这一种规范,有多个版本,在 ES6 当中已经纳入规范,原生支持 Promise 对象。

Promise 可以将回调变成链式调用写法,流程更加清晰,代码更加优雅。

简单归纳下 Promise:三个状态两个过程一个方法,快速记忆方法:3-2-1

三个状态:pendingfulfilledrejected
两个过程:pending→fulfilled(resolve) pending→rejected(reject)
一个方法:then

当然还有其他概念,如catch、 Promise.all/race,这里就不展开了。