上一篇对ES6做了一个笔录,相对新特性,JS目前主要遵循的ES5不算复杂,也就没必要做那么详细的笔录。有必要可以详细阅读阮一峰老师的《JavaScript 教程》,再次感谢阮老师的付出。
JavaScript的核心是面向对象的,它提供了强大的OOP语言能力。ES标准库里面提供了Object、Array、Boolean、Number、String、Date、RegExp、JSON、Error等本地对象以及Math、Global等内置对象。在阮老师的教程中将Object的一些属性和方法归在了属性描述对象,将Number、String、Boolean归为包装对象。再加上W3C标准的DOM和不标准的BOM的宿主对象,构成了JavaScript。
闭包
闭包是JavaScript中最抽象的概念之一,也是最容易造成困惑的。闭包就是一个函数与其被创建时所带有的作用域对象的组合,简单地理解就是定义在一个函数内部的函数。
function F() {
var name = 'OSCHINA';
function displayName() {
console.log(name);
}
return displayName;
}
var f = F();
f();
闭包比较难理解的是它的执行上下文,最终指向谁,须根据当时的执行环境决定。
let name = 'Window';
let object = {
name: 'Object',
getNameByES5() {
return function() {
return this.name;
};
},
getNameByES6() {
return () => {
return this.name;
};
}
};
object.getNameByES5()() // Window
object.getNameByES6()() // Object
闭包常用来模拟私有方法,但注意闭包会在函数外部改变函数内部的值。
let Counter = (function() {
let co = 0;
function change(v) {
co += v;
}
return {
increment() {
change(1);
},
decrement() {
change(-1);
},
value() {
return co;
}
}
})();
闭包常见问题
在let之前,在循环中创建闭包会出现一个作用域的问题,如下:
function help() {
var arr = []
for(var i = 0; i < 10; i++) {
arr[i] = function() {
console.log(i)
}
}
return arr
}
var h = help()
h[0]() // 10
h[1]() // 10
出现上面结果的原因:数组中的闭包是由他们的函数定义和函数help作用域中捕获的环境所组成。三个闭包共享了同一个词法作用域,i的值是被共享的,所以最终都打印10。解决办法就是使用一个工厂函数或者IIFE函数,创造一个新的作用域,或者使用ES6的let关键字。
另外一问题就是闭包的内存消耗。如闭包第一个例子所见,外部函数F被调用赋值给f保持在内存,f依赖与F,于是F不会被清除掉。而且在IE中还可能导致内存泄漏。所以最好是退出函数之前,将不用的局部变量全部删除。
另外在创建类或新对象时,方法关联到原型,而不是构造器中。在构造器中每次调用构造函数,方法都会被重新赋值。
function F(name) {
this.name = name;
this.getName = function() {
return this.name;
}
}
// 没必要用到闭包
function F(name) {
this.name = name;
}
F.prototype = {
getName() {
return this.name;
}
};
// prototype不建议重新定义,这会改变原有的某些定义
function F(name) {
this.name = name;
}
F.prototype.getName = function() {
return this.name;
}
原型和原型链
JS中万物都是对象,有人认为它的根源就是Object,真的是这样吗?先来看一段代码。
function f1() {}
var f2 = function() {}
var f3 = new Function('str')
var o1 = {}
var o2 = new Object()
var o3 = new f1()
typeof Object // function
typeof Function // function
typeof f1 // function
typeof f2 // function
typeof f3 // function
typeof o1 // object
typeof o2 // object
typeof o3 // object
这里有个很有意思的东西,Object和Function都是构造函数,也就是Function的实例。那么JS万物皆对象又怎么讲?
// 构造函数都是Function的实例对象
Object.__proto__ === Function.prototype
Function.__proto__ === Function.prototype
// Function.prototype是Object的实例对象
Function.prototype.__proto__ === Object.prototype
// 将Function.prototype等式替换
Object.__proto__.__proto__ === Object.prototype
Function.__proto__.__proto__ === Object.prototype
// Object.prototype再往上找
Object.prototype.__proto__ === null
由上面可以得出,Object和Fucntion就是由Function创建的对象。JS设计将继承发生在原型和对象上,明面上和Function没有关系。Function对象相当于加工厂,constructor最终指向它,function负责代言,Object负责发言。
// instanceof 用于测试构造函数prototype属性是否出现在对象的原型链上
Function instanceof Object // true
Object instanceof Function // true
那么,__proto__和prototype又是什么呢?__proto__并非ES标准属性,但是被大多现代浏览器实现,用来指向原型对象(prototype)。每个对象实例都有一个私有属性__proto__,包括原型对象,层层向上直到null(原型链顶端)。只有函数才有原型对象,原型对象指向原型的属性和方法,函数创建的时候默认或创建一个原型对象。
function F() {}
var f = new F()
var o = new Object()
// Function
Function // function Function() { [navtive code] }
Function.constructor // function Function() { [navtive code] }
Function.__proto__ // function() { [navtive code] }
Function.prototype // function() { [navtive code] }
Function.constructor === Function // true
Function.prototype === Function.__proto__ // true
// function Function.prototype
Function.prototype.constructor // function Function() { [navtive code] }
Function.prototype.__proto__ // { constructor: function Object() {...}, ... }
Function.prototype.prototype // undefined
Function.prototype.constructor === Function // true
Function.prototype.__proto__ === Object.prototype // true
// function F
F // function F() {}
F.constructor // function Function() { [navtive code] }
F.__proto__ // function() { [navtive code] }
F.prototype // { constructor: function F() {...}, __proto__: {...} }
F.constructor === Function // true
F.prototype.constructor === F // true
F.__proto__ === Function.prototype // true
F.__proto__ === Function.__proto__ // true
// Object f
f // {}
f.constructor // function F() {}
f.prototype // undefined
f.__proto__ // { constructor: function F() {...}, __proto__: {...} }
f.constructor === F // true
f.__proto__ === F.prototype // true
// function Object
Object // function Object() { [navtive code] }
Object.constructor // function Function() { [navtive code] }
Object.prototype // { constructor: function Object() {...}, ... }
Object.__proto__ // function() { [navtive code] }
Object.constructor === Function // true
Object.prototype.constructor === Object // true
Object.__proto__ === Function.prototype // true
Object.__proto__ === Function.__proto__ // true
// object o
o // {}
o.constructor // function Object() { [navtive code] }
o.prototype // undefined
o.__proto__ // { constructor: function Object() {...}, ... }
o.constructor === Object // true
o.__proto__ === Object.prototype // true
通过上面代码可以知道,function和Object通过new创建的对象的__proto__都指向Function.prototype,Function.prototype的__proto__指向Object.prototype。Function.prototype的构造函数指向的是function和Object自身,那么new出来的对象__proto__则指向它们的原型对象,于是形成了原型链。
f.__proto__ -> F.prototype.__proto__ -> Object.prototype.__proto__ -> null
最让人误解的是Function的构造函数是自己,让人以为Function自己创建了一个自己。因为没有类的概念,所以Function自己都很难撇清楚。
Object.prototype.a = function() {}
Function.prototype.b = function() {}
let F = new Function()
let f = new F()
f.a // function () {}
f.b // undefined
封装和继承
JavaScript的封装很简单,但是封装出好的库还是不容易。
// 生成实例对象的原始模式
let Persion = {
name: '',
greeting(str) {
alert(str)
}
}
// 原始模式的改进-工厂模式
function Persion(name) {
return {
name,
greeting(str) {
alert(str)
}
}
}
// 构造函数模式
function Person(name) {
this.name = name
this.greeting = function(str) {
alert(str)
}
}
// Prototype模式
function Person(name) {
this.name = name
}
Person.prototype = {
constructor: Person,
greeting(str) {
alert(str)
}
}
// 类jQuery封装
let Person = (function(w) {
let Person = function(name) {
return new Person.fn.init(name)
}
Person.fn = Person.protype = {
constructor: Person,
init(name) {
this.name = name
this.greeting = function(str) {
alert(str)
}
}
}
Person.fn.init.prototype = Person.fn
return Person
})()
封装好了,如何进行继承扩展呢?类似jQuery的扩展是通过内置extend()函数来进行的,它事实上就是在合并,最简单的方式就是直接挂在jQuery对象上,有兴趣的可以去看下jQuery源码。
// 构造函数绑定
function Male(name, lan) {
Person.apply(this, arguments)
this.name = name
this.lan = lan
}
// prototype模式
Male.prototype = new Person()
Male.prototype.constructor = Male
// 直接继承prototype
Male.prototype = Person.prototype
Male.prototype.constructor = Person
// 使用空对象作为中介
let F = function() {}
F.prototype = Person.prototype
Male.prototype = new F()
Male.prototype.constructor = Male
// 拷贝继承
function extend(child, Parent) {
let p = Parent.prototype
let c = Child.prototype
for(let i in p) {
c[i] = p[i]
}
c.uper = p
}
extend(Male, Person)
上面都是基于构造函数,如果是对象怎么实现继承呢?
// object()方法
function object(o) {
function F() {}
F.prototype = o
return new F()
}
// 浅拷贝
function extend(p) {
let c = {}
for(let i in p) {
c[i] = p[i]
}
c.uper = p
return c
}
// 深拷贝
function deep(p, c) {
let c = c || {}
for(let i in p) {
if(typeof p[i] === 'object') {
c[i] = (p[i].constructor === Array) ? [] : {}
deep(p[i], c[i])
} else {
c[i] = p[i]
}
}
return c
}
在ES6标准中已经提出了新的解决方案class和extend来进行封装继承,所涉及的相关内容可参阅ES6。
需要提一下new关键字的原理:新建一个空对象,将构造函数的作用域赋给新对象,this指向新对象,执行构造函数中的代码,返回新对象。
并发模型和事件循环
在事件循环期间,运行时从最先进入队列的消息开始处理队列中的消息。为此,这个消息会被移除队列并作为输出参数调用与之关联的函数。调用一个函数总是会为其创造新的栈帧,函数的处理会一直进行到执行栈再次为空为止,然后事件循环将会处理队列中的下一个消息。
JS的并发模型基于事件循环,之所以称之为事件循环,是因为它经常按照如下方式来被实现。
while(queue.waitForMessage) {
queue.processNextMessage()
}
如果当前没有任何消息,queue.waitFroMessage会同步地等待消息到达。当一个事件发生且绑定监听器,消息会被加入队列。如果没有事件监听器,事件会丢失。
setTimeout的延迟时间代表消息被加入队列的最小延迟时间,如果队列中没有其他消息,延迟时间过去消息会被马上处理。但是如果有其他消息,须等待其他消息处理完。所以,setTimeout的延迟时间并非确切的等待时间,包括值设为0的时候。
一个web work或者一个跨域的iframe都有自己的栈、堆和消息队列,两个不同的运行时只能通过postMessage方法进行通信。如果另一个运行时侦听message事件,则此方法会向该运行时添加消息。
最后
JS三个部分API众多,不大可能也没必要全部都记住,知道有这个东西就好,在实战中用多了就熟了。加油,咬着苹果的狗!