你不知道的javascript(上)
一、作用域与闭包
1.编译原理
一般编译分为三个步骤:
a.分词、词法分析(Tokenizing/Lexing)
这个过程会将整个代码(字符组成的字符串)分解成有意义的代码(对编程语言来说),这些代码块被称为词法单元(token)。
例如:代码块 var a = 2;这段程序通常会被分解成下面这些词法单元:var、a、=、2、;。空格是否会被当做词法单元,取决于空格在这门语言中是否具有意义。
b.解析/语法分析(Parsing)
这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的数。这个数被称为"抽象语法树"(Abstract Syntax Tree,AST)
例如:var a = 2;的抽象语法树中,可能会生成如下图所示的AST:
c.代码生成
这个过程会将AST转换成为可执行代码,这个过程与语言、目标平台等息息相关。
抛开具体细节,就是通过某种方法,将var a = 2;的AST转换为一组机器指令,用来创建一个叫做a的变量(包括内存分配),然后将2存储其中。
2.理解作用域
解释代码需要理解的三个演员:
引擎:重头到尾负责整个JavaScript程序的编译以及执行过程。
编译器:负责语法分析及代码生成。
作用域:负责收集并维护所有声明的标识符(变量)组成的一系列查询(作用域链的生成)。
3.词法作用域
词法作用域意味着作用域是由书写代码时变量的声明位置所决定的。因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。特殊情况下,修改或影响词法作用域会导性能下降。
a.查找
在多层的嵌套作用域中可以定义同名的标识符,这就叫做"屏蔽效应",该效果的达成依赖于作用域链
b.欺骗词法
使用eval(..)函数,用来动态执创建的代码,当改代码中出现变量声明时,就会改变原有的词法作用域,类似的还有setTimout(..)第一个参数(写字符串时),newFunction的第二个参数(写字符串时),它们所带来的好处无法抵消性能上的损失。
例如:
var a = 10;
function fn(str){
eval(str) // 修改了原本的词法作用域
console.log(a); // 20
}
fn('var a = 20;')
使用with关键字,创建新的词法作用域
function fn(obj){
with(obj){
a = 99 // 当 obj是b时,a被添加到了全局对象中
}
}
var o1 = {a:1}
var o2 = {b:2}
fn(o1);
fn(o2);
console.log(o1.a);
console.log(o2.a);
console.log(window.a)
c.性能
JavaScript引擎会在编译阶阶段进行数项的性能优化,其中就有一些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的位置,才能在执行过程中快速的找到标识符。所以,当引擎在代码中发现了eval(..)或with,它就只能简单的假设预先对标识符位置的判断都是无效的,所有的优化都可能是无意义的,因此最简单的做法就是完全不做任何优化。建议不要使用它们。
4.函数作用域与块级作用域
a.隐藏内部实现
遵循最小特权原则,最小限度的暴露必要内容。将部分代码封装在函数中,形成一个函数作用域。
b.立即执行函数表达式
立即执行函数表达式(Immediately Invoked Function Expression,IIFE)
使用:
var a = 10;
(function IIFE(global){
var a = 20;
console.log(a); // 20
console.log(global.a); // 10
})(window)
c.块级作用域
let、const所声明的变量会隐式的绑定在一个已经存在了的作用域上,通常是{..}内部,块级作用域,也有利于对不再使用的变量回收。
使用:
{
let a = 10;
var b = 1;
const c = 99;
}
// 但运行到这里是,a与c都会被gc回收
console.log(b);
console.log(a); // 会报错
console.log(c);
5.声明提升
例如:var a = 2;会被当作var a和a = 2单个单独的声明,第一个是编译阶段的任务,第二个是执行阶段的任务,通过预编译四部曲,而可以达到符合js中变量提升的效果:
1.创建GO/AO对象
2.找形参和变量声明,将变量和形参名作为AO属性名,值为undefined
3.将实参值和形参统一
4.在函数体里面找函数声明,值赋予函数体
6.作用域闭包
a.理解闭包
闭包定义:当函数可以记住并访问所在它的词法作用域时,就产生了闭包,即使函数是在当前此法作用域之外执行。
闭包展示:
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2 这就是闭包的效果
b.闭包与循环
问题代码:
for (var i = 0; i < 6; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
这段代码,期望是生成数字1~5,每秒一个,但结果却是5个6,这个6的来源是,当跳出循环后,i=6,因为这里的定时器回调函数共用了同一个词法作用域的变量i,而且setTimeout又是宏任务,是在循环完成之后执行的,所有就打印了5个6;
知道了缺陷的原因,这里利用IIFE解决该缺陷,使用IIFE给每一次的循环,都创建一个独立的词法作用域,这样就达到了期望的效果
for (var i = 1; i < 6; i++) {
(function IIEF(i) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
})(i)
}
也可以使用let声明变量,这样let声明的变量每次也就能依附于一个块级作用域,就是每次循环的{..}上,也能达到期望的效果:
for (let i = 1; i < 6; i++) {
setTimeout(function () {
console.log(i);
}, i * 1000);
}
for循环头部的let声明还会有一个特殊的行为,变量在循环过程中,每次迭代都会声明,随后的每次迭代都会使用上次迭代结束时的值来初始化这个变量。
c.闭包
利用闭包特性,作用一个简易的模块管理器:
var MyModules = (function Menager() {
var modules = {};
function define(name, deps, impl) {
for (var i = 0; i < deps.length; i++) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply(impl, deps);
}
function get(name) {
return modules[name];
}
return {
get,
define
}
})();
MyModules.define('bar',[],function(){
function hello(who){
return 'Let me introduce: ' + who;
}
return {
hello
}
});
MyModules.define('foo',['bar'],function(bar){ //当函数中只有一个元素时, bar == [bar]
var hungry = 'hippo';
function awesome(){
console.log(bar.hello(hungry).toUpperCase());
}
return {
awesome
}
})
var bar = MyModules.get('bar');
console.log(bar.hello('jiang'));
var foo = MyModules.get('foo');
foo.awesome();
未来模块机制,ES6的模块机制:
// bar.js
function hello(who){
return "Hello " + who;
}
export default hello;
// foo.js
import hello from './bar.js';
var hungry = 'hippo';
function awesome(){
console.log(
hello(hungry).toUpperCase()
)
}
export default awesome;
// baz.js
import foo from './foo.js';
foo();
使用node命令运行baz.js时,需要间package.json中的type配置改为"module",才可以运行。
7.其它
a.动态作用域
JavaScritpt的作用域就是词法作用域,也是静态作用域,是在写代码(分词时/编译阶段)就确定了的,而动态作用域,this就是很好的体现,需要在运行时才能确定
b.块作用域的替代方案
将ES6的代码转成ES6之前环境能运行的新式:
// ES6
{
let a = 2;
console.log(a);
}
console.log(a);
// ES6之前
try{throw 2}catch(a){
console.log(a);
}
console.log(a);
这里为什么不使用IIFE来创建作用域呢?首先,try/catch的性能的确很糟糕(已经做了改进),其次IIFE与try/catch并不完全等价,如果将任意一段代码的一部分拿出来用函数包裹,会改变这段代码的含义,其中this、return、break/continue都会发生变化,所以IIFE并不是一个普适的解决方案,他只适合在某些特定情况下进行手动操作。
c.箭头函数中的this绑定
这里演示函数对象记录自身被调用的次数
var obj = {
count: 0,
cool: function coolFn() {
// setTimeout(() => {
// this.count++;
// console.log(this.count);
// }, 1000);
setTimeout((function timer() {
console.log(this)
this.count++;
console.log(this.count);
}).bind(this), 1000);
}
}
obj.cool();
二、this
1.this
this是在运行时绑定的,它的绑定取决于函数的调用的方式,与函数的声明没有关系。
常见的this绑定的四种方式:
默认绑定:
var a =10;
function foo(){
console.log(this.a); // this == .window
}
foo()
隐式绑定:
function foo(){
console.log(this.a); // 这里的this为obj
}
var obj = {
a: 2,
foo: foo
}
obj.foo();
显示绑定
使用call(..)、apply()、bind(..)函数绑定
function foo(){
console.log(this.a); // 这里的this为obj
}
var obj = {
a: 8,
}
foo.apply(obj);
new绑定:
使用new关键字,对函数进行构造调用,会自动执行一下操作:
1.在函数首航创建一个全新的对象,例如 var this = {};
2.该队行会被执行[[原型]]连接
3.这个新对象会绑定到函数调用的this
4.如果被new调用的函数没有显示返回其它对象,则会自动返回一个新对象
代码中使用:
function foo(){
this.a = 10;
console.log(this.a); // 这里的this为obj
}
var obj = new foo();
2.被忽略的this
当把null或者undefined作为this的绑定对象传入call、apply、bind时,这些值在调用时会被忽略,实际运行用的是默认绑定规则:
function foo(){
console.log(this.a);
}
var a = 2;
foo.call(null);
使用apply(..)来展开数组,bind(..)可以实现函数的柯里化(预先设置一些参数):
function foo(a,b){ // 展开数组
console.log('a:' + a, 'b:' + b);
}
foo.apply(null,[2,3]);
var bar =foo.bind(null,2); //函数柯里化
bar(3);
总是使用null来忽略this绑定可能会产生一些副作用,当函数内确实使用了this时,this将会绑定到window全局对象,就有可能修改全局对象,这种当然不好。创建一个空的非委托对象代替null,将会更加安全:var DMZ = Object.create(null);
如:
var DMZ = Object.create(null);
function foo(a,b){ // 展开数组
this.a = 10;
console.log('a:' + a, 'b:' + b);
}
foo.apply(DMZ,[2,3]);
3.软绑定
给默认绑定指定一个全局对象和undefined以外的值,不仅可以实现和硬绑定相同的效果,还同时保留了隐式绑定或者显示绑定修改this的能力。
if (!Function.prototype.softBind) {
Function.prototype.softBind = function (obj) {
var fn = this;
var curried = [].splice.call(arguments, 1);
var bound = function () {
return fn.apply(
(!this || this === (window || global)) ? obj : this,
curried.concat.apply(curried, arguments)
)
}
bound.prototype = Object.create(fn.prototype);
return bound;
}
}
function foo() {
console.log("name:" + this.name);
}
var obj = { name: 'obj' };
var obj2 = { name: 'obj2' };
obj2.bar = foo.softBind(obj);
obj2.bar();
setTimeout(obj2.bar, 1000);
4.箭头函数
箭头函数的this绑定时书写箭头函数的的位置,根据外层(函数或者全局)作用域来决定,任何方式不能再被修改。
看一个示列代码:
function foo(){
return (a)=>{
console.log(this.a);
}
}
var obj1 = {
a:2
}
var obj2 = {
a:3
}
var bar = foo.call(obj1); // 这里已经确定了箭头函数的this指向了obj1,不能再被改变了
bar.call(obj2); // 2 而不是 3
在es6之前,使用的类似域箭头函数的模式:
function foo() {
var self = this;
setTimeout(function () {
console.log(self.name);
}, 1000);
}
var obj = {
name: 'obj',
}
foo.call(obj);
三、对象
1.数据类型
JS中基本数据类型:string、number、boolean、object、null、undefined,JS内置对象(内置函数,可new):String、Number、Boolean、Object、Function、Array、Date、RegExp、Error
使用:
var strPrimitive = "I am string";
typeof strPrimitive; // "string"
strPrimitive instanceof String; // false
var strObject = new String("I am String");
typeof strObject; // "object"
strObject instanceof String; // true
类型转换,当在使用字面量的属性或方法时,会使用对应的内置构造函数将其包装成对象后调用属性或函数:如
console.log('I am string'.length); // ==> new String('I am string').length;
console.log(42.324.toFixed(2)); // ==> new Number(42.324).toFixed()
不同的是,null和undefined没有对应的构造形式它们之后文字形式。相反,Date只有构造,没有文字形式。对于Object、Array、Function、RegExp来说,无论是文字形式还是构造函数新式,它们都是对象。Error对象一般都是出错时被自动创建,也可以使用new Error(..)手动创建,使用时用throw关键字抛出:throw new Error(..)
2.属性描述符
属性描述符包括"数据描述符"与"访问描述符"。
属性描述符:
let obj = Object.create(null);
Object.defineProperty(obj, 'name', {
value: '张三', // 属性值
writable: false, // 属性是否可修改
configurable: false, // 属性是否可删除,以及属性描述符是否可修改
enumerable: true // 属性是否可枚举
});
访问器描述符(两种方式定义):
var obj = {
get name() {
return _name_;
},
set name(value) {
_name_ = value;
}
}
obj.name = 'jiang';
Object.defineProperty(obj,'age',{
get : function(){
return _age_;
},
set : function(value){
_age_ = value;
}
})
obj.age = 18;
console.log(obj);
3.遍历
这里介绍一下数组的for..of遍历,for..of循环首先会向被访问对象请求一个迭代器对象@@iterator,然后通过迭代器对象的next()方法来遍历所有返回值:如下
var myArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var it = myArray[Symbol.iterator]();
while (true) {
var result = it.next();
if (result.done) break;
console.log(result.value);
}
但是普通的对象却没有内置的@@iterator,当想要做这样循环的操作,也可以给想遍历的对象定义一个@@iterator对象,如下:
var myObject = {
name: "John",
age: 34,
isMarried: false
};
Object.defineProperty(myObject, Symbol.iterator, {
enumerable: false,
writable: false,
configurable: true,
value: function () {
var o = this;
var idx = 0;
var ks = Object.keys(o);
return {
next() {
return {
value: o[ks[idx++]],
done: idx > ks.length
}
}
}
}
})
for (let key of myObject) {
console.log(key);
}
利用for..of循环每次调用迭代器对象的next()方法时,内部指针都会向前移动并返回对象属性列表的下一个值,利用这个特性制作一个永远不会结束的迭代器(比如返回随机数、递增值、唯一标识符等等):
var randoms = {
[Symbol.iterator]:function(){
return {
next(){
return {
value:Math.random(),
done:false
}
}
}
}
}
var random_pool = [];
for(let n of randoms){
random_pool.push((n*100).toFixed(2));
if(random_pool.length>=100){
break;
}
}
console.log(random_pool);
四、原型
1.屏蔽与属性设置
JS中,当出现myObject.foo = "bar"赋值操作时会出现的三种情况:
1.如果在[[Prototype]]链上层存在名为foo的普通数据访问属性,该属性是的非只读(writalbe)时,那就会直接在myObject中添加一个名为foo的新属性,它是屏蔽属性。
2.如果在[[]Prototype]链上层存在foo,但是它被标记为只读(writable)时,那么无法修改已有属性或者在myObject上创建屏蔽属性。非严格模式下该赋值语句会被忽略,严格模式下会抛出一个错误。当然这主要是为了模拟类属性的继承。
3.如果在[[Prototype]]链上层存在foo并且它是一个setter,那就一个会调用这个setter,foo不会添加到(或者说屏蔽于)muObject,也不会重新定义foo这个setter
第二种与第三种情况下也可以屏蔽foo,但是不能用=操作符来赋值,而是用Object.defineProperty(..)来向myObject添加foo。
2.继承
a.instanceof函数
使用instanceof从"类"角度判断a的"祖先"(委托关联):
function Foo() { };
var a = new Foo();
console.log(a instanceof Foo);
// 这里使用bind函数,并不会有任何影响,new对this的绑定优先级高于bind函数对this的绑定
var l = { a: 10 };
function Foo() { }
var a = new (Foo.bind(l));
console.log(a)
console.log(a instanceof Foo);
instanceof在这里回答了:在a的整条[[Prototype]]链中是否有指向Foo.prototype的对象?
b.isPrototypeOf函数
使用isPrototypeOf直接判断一个对象是否在另一个对象的原型链上:
var myObject = {};
var anotherObject = {};
Object.setPrototypeOf(myObject, anotherObject);
console.log(anotherObject.isPrototypeOf(myObject));
c.__proto__实现
在对大多数浏览器中也支持一个种非标准的方法来访问内部的[[Protoype]]属性,功能与Object.getPrototypeOf(..),它就是__proto__属性,它并不能存在于你正在使用的对象中,而是和他其常用函数(.toString(..)、isPrototypeOf(..),等等)一样,存在于内置的Object.prototype中。__proto__的实现大致如下:
Object.defineProperty(Object.prototype, "_proto_", {
get: function () {
return Object.getPrototypeOf(this);
},
set: function (value) {
Object.setPrototypeOf(this, value);
}
})
console.log({}._proto_);
d.create使用
Object.create(..)是在ES5中新增的函数,在ES5之前环境中使用,就需要一段简单的polyfill代码,它部分实现了Object.create(..)的功能:
Object.create = function(obj){
function F(){}
F.prototype = obj;
return new F();
}
这里polyfill的函数,传入null时,并不能创建一个"比较空的对空对象",且真正的Object.create(..)函数的第二个参数可一个定义新对象包含的属性及属性描述符。使用:
console.log(Object.create(null)); // 适用于用来装数据,屏蔽原型链的干扰
var anotherObject = {};
var myObject = Object.create(anotherObject, {
"name": {
value: "Nicholas",
writable: true,
enumerable: true,
configurable: true
},
"age": {
value: 29,
writable: true,
enumerable: true,
configurable: true
}
});
console.log(myObject);
e.行为委托
需求:通用控件行为的父类(如Widget)和继承父类的特殊控件子类(如Button)
在JS中下实现类风格代码:
控件"类"
// 父类
function Widget(width,height){
this.with = width || 50;
this.height = height || 50;
this.$elem = null;
}
Widget.prototype.render = function($where){
if(this.$elem){
this.$elem.css({
width:this.width + "px",
height:this.height + "px"
}).appendTo($where);
}
};
// 子类
function Button(width,height,label){
Widget.call(this,width,height);
this.label = label || "Default";
this.$elem = $('
使用对象关联风格委托来实现Wight/Button:
// 委托控件对象
var Widget = {
init: function (width, height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
},
insert: function ($where) {
if (this.$elem) {
this.$elem.css({
width: this.width + "px",
height: this.height + "px"
}).appendTo($where);
}
}
}
var Button = Object.create(Widget);
Button.setup = function (width, height, label) {
// 委托调用
this.init(width, height);
this.label = label || "Default";
this.$elem = $('