上篇聊的HTML,CSS 这篇聊聊JS
提纲:
变量类型
原型、原型链
作用域和闭包
异步
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为上下文中的thissetTimeout, 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指向bapply
: 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
三个状态:pending
、fulfilled
、rejected
两个过程:pending→fulfilled(resolve)
pending→rejected(reject)
一个方法:then
当然还有其他概念,如catch、 Promise.all/race,这里就不展开了。