《你不知道的JavaScript 上》读书笔记

标签: js 还有原型链要更新呀


作用域

  • 作用域是查找变量的一套规则
  • 如果查找的目的是对变量进行赋值,则进行LHS查询;如果查找的目的是获取变量的值,则进行RHS查询。Left/Right Hand Sidei,变量在等号左边或等号右边
  • LHS查询:赋值操作,如=操作符、调用函数时传参
 function foo(e) {
    var b = a;
    return a + b;
}
var c = foo(2);
  • 例子中LHS查询有三处:b=a c=foo(2) 把2赋给a(隐式变量分配)
  • RHS查询有四处:foo(2) =a a b
  • 异常
    • ReferenceError,引用一个未被定义的变量。
    • TypeError,对结果的操作不合理,如引用一个undefined类型的值的属性
    • SyntaxError,语法错误
  • 如何判断当前是否在严格模式下:函数直接调用时this指向运行时环境的全局对象。在非严格模式下,浏览器中全局对象为Window,Nodejs控制台中全局对象为Global;在严格模式下,全局对象是undefined。因此可判断如下
var isStrict = (function(){return !this;}());
console.log('isStrict ' + isStrict);
  • 编译。传统编译语言,代码执行前需要编译,包括三个过程:词法分析(将代码拆分为词法单元)、语法解析(将词法单元数组转换为语法树)、代码生成(生成可执行代码)

词法作用域VS动态作用域

  • 编译的词法分析阶段知晓各标识符及如何声明的,因此可以在执行过程中进行查找。词法作用域意味着作用域在书写代码时函数声明的位置决定。
  • eval with
  • 词法作用域又叫静态作用域,和动态作用域的区别是动态作用域是在运行时而非定义时确定的。词法作用域关注函数在何处声明,动态作用域关注函数从何处调用,其作用域链是基于运行时的调用栈
  • wiki:大多数现在程序设计语言都是采用静态作用域规则,如C/C++、C#、Python、Java、JavaScript;采用动态作用域的语言有Pascal、Emacs Lisp、Common Lisp(兼有静态作用域)、Perl(兼有静态作用域)。C/C++是静态作用域语言,但在宏中用到的名字,也是动态作用域。
// 词法作用域:
function foo() {
    print a;    // 输出2
}

function bar() {
    var a = 3;
    foo();
}

var a = 2;
bar();
// 动态作用域
function foo() {
    print a;    // 输出3而不是2
}

function bar() {
    var a = 3;
    foo();
}

var a = 2;
bar();

函数作用域

  • 最小特权原则
  • 在软件设计中,应该最小限度地暴露必要内容,而将其隐藏起来,比如某个模块或对象的API设计。
  • 避免暴露私有的变量或函数,避免其被有意或无意地以非预期的方式使用。
function doSomething(a){
    b = a + doSomethingElse(a * 2);
    console.log(b * 3);
}
function doSomethingElse(e){
    return e - 1;
}
var b;
doSomething(2); // 15
  • 上例中b doSomethisElse均应是doSomething的内部内容,改进:
function doSomething(a){
    function doSomethingElse(e){
        return e - 1;
    }
    var b;
    b = a + doSomethingElse(a * 2);
    console.log(b * 3);
}
doSomething(2); // 15           
  • 立即执行函数表达式IIFE 的两种形式
(function(){console.log('a');})() // a
// 第一个()将函数变成表达式,第二个()立即执行这个函数

+function(){console.log('a');}() // a
// + 也会将函数变成一个表达式,js会解析成运算(bootstrap的js源码采用此种方式),第二个()立即执行这个函数

js中神奇的 +
new Date(); // Fri Aug 05 2016 16:58:08 GMT+0800 (CST)
+ new Date; // 1470387494773 (一个时间戳)

// 时间戳:unix时间,从`协调时间时`1970年1月1日0时0分0秒起至现在的总秒数
  • IIFE进阶1:当作函数调用并传参,提高可读性,改进代码风格。
var a = 2;
(function IIFE(global){
    var a = 1;
    console.log(a); // 1
    console.log(global.a); // 2
})(window);
  • IIFE进阶2:倒置代码的运行顺序,将需要运行的代码放在第二位,在IIFE执行后当作参数传递进去
var a = 2;
(function IIFE(def){
    def(window);
    console.log(a); // 2
}(function def(global){    
    var a = 1;
    console.log(a); // 1
    console.log(global.a); // 2
}));

块级作用域

let ES6变量声明

  • 隐式绑定在所在块级作用域
var foo = true;
if (foo) {
    var bar = foo * 2;
    console.log(bar); // 2
}
console.log(bar); // 2
var foo = true;
if (foo) {
    let bar = foo * 2;
    console.log(bar); // 2
}
console.log(bar); // ReferenceError: bar is not defined
  • 显式创建一个块级作用域
var foo = true;
if (foo) {
    {
        let bar = foo * 2;
        console.log(bar); // 2
    } // 一个显式的块
    console.log(bar); // ReferenceError: bar is not defined
}
console.log(bar); // ReferenceError: bar is not defined
  • let声明的变量在块级作用域中不会提升
  • 垃圾回收,let块级作用域中的内容执行完后被销毁
  • let循环
for(let i=0;1<10;i++) {
    console.log(i);
}
console.log(i); // ReferenceError: i is not defined
  • let将i绑定在for循环的块中,不会污染全局变量,并且将其重新绑定在循环的每一次迭代中。

const ES6变量声明

  • const声明的变量只在声明时赋值,不可修改,否则会报错
  • const声明的变量必须在声明时就赋值。
var foo = true;
if(foo) {
    var a = 2;
    const b = 3;
    const c = {};
    const d; // SyntaxError: Missing initializer in const declaration
    a = 3;
    b = 4; // TypeError: Assignment to constant variable
    c.a = 1; 
    console.log(c); // {a: 1}
    console.log(c.a); // 1
}
console.log(a); // 3
console.log(b); // ReferenceError: b is not defined

提升(hoisting)

  • 理解提升:引擎会在解释js代码前首先进行编译,编译的一部分工作就是找到所有声明,并用合适的作用域将他们包裹起来。所有声明会在代码执行前被处理。
  • 编译阶段声明会被提升,赋值或其他运算逻辑留在原地在执行过程中被处理。
  • 函数声明提升的同时,包含了实际函数的隐藏值。
foo(); // undefined;
function foo() {
    console.log(a);
    var a = 1;
}
// 此时foo()可正常调用,foo()函数内部也有变量提升
  • 函数表达式和变量声明类似,变量标识符会提升,赋值没有被提升
foo(); // TypeError: foo is not a function
var foo = function() {
    console.log(a);
    var a = 2;
}
// 此时报错不是ReferenceError,因为此时变量标识符foo被提升到了顶部,var foo;但赋值未被提升,此时foo的值是undefined,对一个undefined值进行函数调用是非法操作,因此抛出TypeError异常
  • 提升中函数优先
foo(); // 1
var foo;
function foo() { 
    console.log(1); 
} 
foo = function() {
    console.log(2);
}

引擎解释:

function foo() { 
    console.log(1); 
} 
foo(); 
foo = function() {
    console.log(2);
}
// var foo;是重复声明被忽略。
foo(); // 此时再调用foo()值为2,第二次定义的函数表达式覆盖了前面的函数声明。
  • 一个函数声明会被提升到所在作用域的顶部
foo(); //  TypeError: foo is not a function
var a = true;
if (a) {
    function foo(){console.log(11);} 
} else {
    function foo(){console.log(22);} // 
}
// 老版本中两个foo函数会被提升,第二个foo函数会覆盖第一个,因此foo()执行结果是22;新版本中if块内仅提升函数声明,但未包含实际函数的隐藏值,因此foo()执行结果是TypeError

作用域闭包

  • 闭包:内部函数可以访问外部函数作用域,并持有对原始词法作用域的引用。
  • 经典例子:循环与闭包
for(var i=0;i<5;i++) {
    setTimeout(function timer() {
        console.log(i);
    }, i*1000);
}
  • 输出五次5的原因:循环中的五个函数在每次迭代中分别定义,但它们被封闭在一个共享的全局作用域,因此共享一个i的引用。
  • 解决方法是给每次循环的函数创建一个单独的作用域
for(var i=0;i<5;i++) {
    (function() {
        var j = i;
        setTimeout(function timer() {
            console.log(j);
        }, j*1000);
    })()
}
// IIFE为每次迭代创建一个新的作用域,延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代都有一个正确的变量。

or

for(let i=0;i<5;i++) { // i在每次迭代都会声明
    setTimeout(function timer() {
        console.log(i);
    }, i*1000);
}

or

for(var i=0;i<5;i++) {
    let j = i; // 创建块级作用域
    setTimeout(function timer() {
        console.log(i);
    }, i*1000);
}

this

  • 隐式传递一个对象引用
  • 一个函数被调用时,会创建一个执行上下文,包含函数在哪里被调用,函数的调用方式,传入的参数等,this是其中的一个属性,是函数被调用时发生的绑定。this既不指向函数本身也不指向函数的词法作用域。
  • 直接调用 new dot(.) call apply bind
function fo(){this.x='x';};
// 直接调用
fo(); // ()代表立即执行
x;    // 'x' this指向全局对象window
var fn = function(){'use strict';this.x='x';};
x;    // ReferenceError: x is not defined,此时全局对象是undefined;

// new调用
var foo = new fo();
foo.x; // 'x'

// dot(.)调用
var aa = {x: 'aaa',fo: fo};
aa.x; // 'aaa'
aa.fo; // function fo(){this.x='x';};
aa.fo();
aa.x; // 'x'

// call
var b = {x: 'bb'};
b.x; // 'bb'
b.fo(); // TypeError: b.fo is not a function
fo.call(b);
b.x; // 'x'

// apply bind 用法和call相似,this均指向fo.call(args),args的第一个参数

对象

  • javascript六种类型:string number boolean null undefined object
  • 内置对象,对象子类型:string number boolean object function array date regexp error
  • ES6增加了可计算属性名,用[]包裹,eg:
var prefix = foo; 
var myObject = {[prefix + 'bar']: 'hello', [prefix + 'baz']: 'world'};
myObject['foobar']; // hello
myObject['foobaz']; // world
  • 数组也是对象,但是最好用对象存储键值对,用数组存储数组下标值对;
// 给数组定义数字形式的属性名的bug
var myArray = ['hello', 'world', 42];
myArray.baz = 'bar';
myArray.length; // 3
myArray.baz; // 'bar'
myArray['3'] = 'test'; 
myArray.length; // 4 
myArray; // ['hello', 'world', 42, 'test']
  • 检查属性是否存在
var myObject = { a: 2 };
myObject['b']; // 2
myObject.b; // 2
'a' in myObject; // true
myObject.hasOwnProperty('a'); // true
// in操作符检查属性是否在对象中或prototype原型链中;hasOwnProperty仅检查属性是否在对象中。
  • 属性描述符 ES5
  • getOwnpropertyDescriptor查询对象的属性的属性描述符,格式:Object.getOwnpropertyDescriptor(对象名,'属性名');
Object.getOwnPropertyDescriptor(myObject,'a');
// Object {
//    value: 2, 
//    writable: true,     可写,可修改属性的值value
//    enumerable: true,   可枚举,出现在对象属性的遍历中
//    configurable: true  可配置,可用defineProperty修改属性描述符,删除一个属性delete myObject.a; if false,除value和writable外的特性不能被改变。
// }
  • defineProperty添加一个新属性或修改一个已有属性(的属性描述符),格式:Object.defineProperty(对象名,'属性名',{});
Object.defineProperty(myObject,'b',{
       value: 3,
       writable: true,
       configurable: true,
       enumerable: true
}); // Object {a: 2, b: 3}
Object.getOwnPropertyDescriptor(myObject,'b');
// Object {value: 3, writable: true, enumerable: true, configurable: true}
  • 枚举属性 enumerable
var myObject = {};

Object.defineProperty(myObject,'a',{
      value: 2,
      enumerable: true
});

Object.defineProperty(myObject,'b',{
      value: 3,
      enumerable: false
});

myObject.b; // 3
'b' in myObject; // true
myObject.hasOwnProperty('b'); // true

for(var k in myObject) {
      console.log(k+': '+myObject[k]);
} // a: 2
// myObject.b存在且可正常访问,但不出现在属性的遍历中

myObject.propertyIsEnumerable('b'); // false
// propertyIsEnumerable检查给定的属性名存在于对象中(不包括原型链中)&& 可枚举的

Object.keys(myObject); // ['a'] 返回可枚举属性

Object.getOwnPropertyNames(myObject); // ['a','b'] 返回所有属性
  • 数组遍历注意事项:数组上应用for..in循环时不仅包含所有数值索引,且包含所有可枚举属性;因此遍历数组最好使用传统for循环或forEach循环。ES6中新增for..of循环遍历数据结构。数组有内置的@@iterator,因此for..of可直接遍历数组,而不能直接遍历对象。P122
var myArray = ['hello','world',42];
myArray.baz = 'bar';
myArray['3']='test';
myArray; // ["hello", "world", 42, "test"]

for(var k in myArray) {console.log(k,myArray[k]);}
//  0 hello  1 world  2 42  3 test  baz bar

for(vari=0;i

原型prototype


你可能感兴趣的:(《你不知道的JavaScript 上》读书笔记)