读《你不知道的javascript》(上)部分东西记录

你不知道的javascript(上)

一、作用域与闭包

1.编译原理

一般编译分为三个步骤:

a.分词、词法分析(Tokenizing/Lexing)
这个过程会将整个代码(字符组成的字符串)分解成有意义的代码(对编程语言来说),这些代码块被称为词法单元(token)。

例如:代码块 var a = 2;这段程序通常会被分解成下面这些词法单元:var、a、=、2、;。空格是否会被当做词法单元,取决于空格在这门语言中是否具有意义。

b.解析/语法分析(Parsing)
这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的数。这个数被称为"抽象语法树"(Abstract Syntax Tree,AST)

例如:var a = 2;的抽象语法树中,可能会生成如下图所示的AST:
读《你不知道的javascript》(上)部分东西记录_第1张图片

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 = $('

你可能感兴趣的:(javascript)