【读书】JavaScript 秘密花园

127 阅读14分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

写在前面:读《JavaScript 秘密花园》,根据自己知识缺漏摘抄部分内容,以及实际代码执行结果

对象

\

toString:所有对象都有toString()方法,基本类型除null和undefined外也有
2.toString() 语法错误因为会把 . 当成数字运算,解决办法

\

2..toString(); // 第二个点号可以正常解析
2 .toString(); // 注意点号前面的空格
(2).toString(); // 2先被计算

\

默认情况下,toString()方法被每个Object对象继承。如果此方法在自定义对象中未被覆盖,toString()返回 "[object type]",其中type是对象的类型
第一个object:ecma规范要求toString 方法返回[object class] ,不过大多数内部类覆盖了toString方法,所以只有自定义对象的会返回[object object] ,客户端内建的对象一般class都是为Object(大写) ,虽然很多类覆盖了toString方法,但是我们可以用Object.prototype.toString.apply(o) 显示调用object.toString。

\

delete obj[属性名]才能删除属性名,该属性设置为null/undefined只是解除与值的关联 。用obj.hasOwnProperty(i)检验

\

原型属性:可以把任何类型的值赋给它(prototype)。 然而将原子类型赋给 prototype 的操作将会被忽略。

\

function Foo() {}
Foo.prototype = 1; // 无效

\

hasOwnProperty:hasOwnProperty 是 JavaScript 中唯一一个处理属性但是不查找原型链的函数。

\

// 修改Object.prototype
Object.prototype.bar = 1; 
var foo = {goo: undefined};

foo.bar; // 1
'bar' in foo; // true
for(let i in foo)
// ——这3个都会查找

\

函数

\

1、将匿名函数赋值给变量

\

foo; // 'undefined'
foo(); // 出错:TypeError
var foo = function() {};

\

由于 var 定义了一个声明语句,对变量 foo 的解析是在代码运行之前,因此 foo 变量在代码运行时已经被定义过了。

\

但是由于赋值语句只在运行时执行,因此在相应代码执行之前, foo 的值缺省为 undefined。

\

命名函数的赋值表达式
2、另外一个特殊的情况是将命名函数赋值给一个变量。

\

var foo = function bar() {
    bar(); // 正常运行
}
bar(); // 出错:ReferenceError

\

bar 函数声明外是不可见的,这是因为我们已经把函数赋值给了 foo; 然而在 bar 内部依然可见。这是由于 JavaScript 的 命名处理 所致, 函数名在函数内总是可见的。
注意:在IE8及IE8以下版本浏览器bar在外部也是可见的,是因为浏览器对命名函数赋值表达式进行了错误的解析, 解析成两个函数 foo 和 bar


this

函数调用的时候内部this指向window
常见误解:

\

Foo.method = function() {
    function test() {
        // this 将会被设置为全局对象(译者注:浏览器环境中也就是 window 对象)
    }
    test();
}

\

为了在 test 中获取对 Foo 对象的引用,我们需要在 method 函数内部创建一个局部变量指向 Foo 对象。

\

Foo.method = function() {
    var that = this;
    function test() {
        // 使用 that 来指向 Foo 对象
    }
    test();
}

\

函数别名:也就是将一个方法赋值给一个变量。

\

var test = someObject.methodTest;
test();

\

上例中,test 就像一个普通的函数被调用;因此,函数内的 this 将不再被指向到 someObject 对象。——指向window(严格模式下是undefined)

\

function Foo() {}
Foo.prototype.method = function() {};

function Bar() {}
Bar.prototype = Foo.prototype;

new Bar().method();

\

当 method 被调用时,this 将会指向 Bar 的实例对象。

\

函数 是 JavaScript 中唯一拥有自身作用域的结构

\

循环中的闭包:

\

for(var i = 0; i < 10; i++) {
    (function(e) {
        setTimeout(function() {
            console.log(e);  // 可以打印1-10
        }, 1000);
    })(i);
}

for(var i = 0; i < 10; i++) {
    setTimeout((function(e) {
        return function() {
            console.log(e);
        }
    })(i), 1000)
}

\

arguments 对象

\

JavaScript 中每个函数内都能访问一个特别变量 arguments。这个变量维护着所有传递到这个函数中的参数列表。

\

注意: 由于 arguments 已经被定义为函数内的一个变量。 因此通过 var 关键字定义 arguments 或者将 arguments 声明为一个形式参数, 都将导致原生的 arguments 不会被创建。

\

arguments 变量不是一个数组(Array)。 尽管在语法上它有数组相关的属性 length,但它不从 Array.prototype 继承,实际上它是一个对象(Object)。

\

因此,无法对 arguments 变量使用标准的数组方法,比如 push, pop 或者 slice。 虽然使用 for 循环遍历也是可以的,但是为了更好的使用数组方法,最好把它转化为一个真正的数组。
传递参数

\

function foo() {
    bar.apply(null, arguments);
}
function bar(a, b, c) {
    // 干活
}

function Foo() {}

Foo.prototype.method = function(a, b, c) {
    console.log(this, a, b, c);
};

// 创建一个解绑定的 "method"
// 输入参数为: this, arg1, arg2...argN
Foo.method = function() {

    // 结果: Foo.prototype.method.call(this, arg1, arg2... argN)
    Function.call.apply(Foo.prototype.method, arguments);
};

// 上面的 Foo.method 函数和下面代码的效果是一样的:
Foo.method = function() {
    var args = Array.prototype.slice.call(arguments);
    Foo.prototype.method.apply(args[0], args.slice(1));
};

\

自动更新
arguments 对象为其内部属性以及函数形式参数创建 getter 和 setter 方法。

\

因此,改变形参的值会影响到 arguments 对象的值,反之亦然。

\

function foo(a, b, c) {
    arguments[0] = 2;
    a; // 2                                                           

    b = 4;
    arguments[1]; // 4

    var d = c;
    d = 9;
    c; // 3
}
foo(1, 2, 3);

\

console.assert() ——如果断言为false,则将一个错误消息写入控制台。如果断言是 true,没有任何反应。

\

arguments.callee:

\

callee 是 arguments 对象的一个属性。它可以用于引用该函数的函数体内当前正在执行的函数。这在函数的名称是未知时很有用,例如在没有名称的函数表达式 (也称为“匿名函数”)内。

\

function foo() {
    arguments.callee; // do something with this function object
    arguments.callee.caller; // and the calling function object
}

function bigLoop() {
    for(var i = 0; i < 100000; i++) {
        foo(); // Would normally be inlined...
    }
}

\

foo 不再是一个单纯的内联函数 inlining(译者注:这里指的是解析器可以做内联处理), 因为它需要知道它自己和它的调用者。 这不仅抵消了内联函数带来的性能提升,而且破坏了封装,因此现在函数可能要依赖于特定的上下文。

\

构造函数:通过 new 关键字方式调用的函数都被认为是构造函数。如果被调用的函数没有显式的 return 表达式,则隐式的会返回 this 对象 - 也就是新创建的对象。
显式的 return 表达式将会影响返回结果,但仅限于返回的是一个对象。

\

function Bar() {
    return 2; //new Bar().constructor === Bar
}
new Bar(); // 返回新创建的对象,不是2

function Test() {
    this.value = 2;

    return {
        foo: 1  // new Test().constructor === Object
    };
}
new Test(); // 返回的对象

(new Test()).value === undefined //true
(new Test()).foo === 1. //true

\

作用域:尽管 JavaScript 支持一对花括号创建的代码段,但是并不支持块级作用域; 而仅仅支持 函数作用域。

\

JavaScript 中没有显式的命名空间定义,这就意味着所有对象都定义在一个全局共享的命名空间下面。

\

每次引用一个变量,JavaScript 会向上遍历整个作用域直到找到这个变量为止。 如果到达全局作用域但是这个变量仍未找到,则会抛出 ReferenceError 异常。

\

// 脚本 A
foo = '42';

// 脚本 B
var foo = '42'

\

脚本 A 在全局作用域内定义了变量 foo,而脚本 B 在当前作用域内定义变量 foo。

\

局部变量:JavaScript 中局部变量只可能通过两种方式声明,一个是作为函数参数,另一个是通过 var 关键字声明。

\

解决命名冲突:匿名包装器

\

(function() {
    // 函数创建一个命名空间

    window.foo = function() {
        // 对外公开的函数,创建了闭包
    };

})(); // 立即执行此匿名函数

\

匿名函数被认为是 表达式;因此为了可调用性,它们首先会被执行。

\

( // 小括号内的函数首先被执行
function() {}
) // 并且返回函数对象
() // 调用上面的执行结果,也就是函数对象

// 另外两种方式
+function(){}();
(function(){}());

\

\

名称解析顺序
比如,当访问函数内的 foo 变量时,JavaScript 会按照下面顺序查找:

\

  1. 1、当前作用域内是否有 var foo 的定义。 2、函数形式参数是否有使用 foo 名称的。 函数自身是否叫做 foo。
    回溯到上一级作用域,然后从 #1 重新开始。

\

注意: 自定义 arguments 参数将会阻止原生的 arguments 对象的创建。

\

数组

\

为了达到遍历数组的最佳性能,推荐使用经典的 for 循环。

\

var list = [1, 2, 3, 4, 5, ...... 100000000];
for(var i = 0, l = list.length; i < l; i++) {
    console.log(list[i]);
}

\

通过 l = list.length 来缓存数组的长度。

\

虽然 length 是数组的一个属性,但是在每次循环中访问它还是有性能开销。 可能最新的 JavaScript 引擎在这点上做了优化,但是我们没法保证自己的代码是否运行在这些最近的引擎之上。

\

实际上,不使用缓存数组长度的方式比缓存版本要慢很多。

\

Array 构造函数

\

var arr = new Array(3);
arr[1]; // undefined
1 in arr; // false, 数组还没有生成

\

new Array(count + 1).join(stringToRepeat);
new Array(3).join('#') 将会返回 ##

\

类型

\

typeof
typeof 返回值不准确,Object.prototype.toString可以返回对象的内部属性 [[Class]] 的值
typeof 唯一适用:检测是否定义。如果没有定义而直接使用会导致 ReferenceError 的异常
typeof foo !== 'undefined'

\

JavaScript 标准文档中定义: [[Class]] 的值只可能是下面字符串中的一个: Arguments, Array, Boolean, Date, Error, Function, JSON, Math, Number, Object, RegExp, String.

\

Object.prototype.toString.call([])    // "[object Array]"
Object.prototype.toString.call({})    // "[object Object]"
Object.prototype.toString.call(2)    // "[object Number]"

function is(type, obj) {
    var clas = Object.prototype.toString.call(obj).slice(8, -1);
    return obj !== undefined && obj !== null && clas === type;
}

is('String', 'test'); // true
is('String', new String('test')); // true

//在 ECMAScript 5 中,为了方便,对 null 和 undefined 调用 Object.prototype.toString 方法, 其返回值由 Object 变成了 Null 和 Undefined。
// IE8
Object.prototype.toString.call(null)    // "[object Object]"
Object.prototype.toString.call(undefined)    // "[object Object]"

// Firefox 4
Object.prototype.toString.call(null)    // "[object Null]"
Object.prototype.toString.call(undefined)    // "[object Undefined]"

\

instanceof
用来比较来自同一个 JavaScript 上下文的自定义对象。只有在比较自定义的对象时才有意义。 如果用来比较内置类型,将会和 typeof 操作符 一样用处不大。

\

类型转换
内置类型(比如 Number 和 String)的构造函数在被调用时,使用或者不使用 new 的结果完全不同。
new Number(10) === 10;     // False, 对象与数字的比较
需要比较的话,最好先显式转换成以下3种:
(1)转换为字符串:'' + 10 === '10'; // true
(2)转换成数字:+'10' === 10; // true

\

+'010' === 10
Number('010') === 10
parseInt('010', 10) === 10  // 用来转换为整数

+'010.2' === 10.2
Number('010.2') === 10.2
parseInt('010.2', 10) === 10

\

(3)转换为布尔型:!!'foo'===true

\

核心

\

eval
eval 函数会在当前作用域中执行一段 JavaScript 代码字符串。

\

var foo = 1;
function test() {
    var foo = 2;
    eval('foo = 3');
    return foo;
}
test(); // 3
foo; // 1

\

但是 eval 只在被直接调用并且调用函数就是 eval 本身时,才在当前作用域中执行。

\

var foo = 1;
function test() {
    var foo = 2;
    var bar = eval;
    bar('foo = 3');
    return foo;
}
test(); // 2
foo; // 3

\

上面的执行作用域相当于全局,等价于window.foo=3或者

\

// 写法二:使用 call 函数修改 eval 执行的上下文为全局作用域
var foo = 1;
function test() {
    var foo = 2;
    eval.call(window, 'foo = 3');
    return foo;
}
test(); // 2
foo; // 3

\

在任何情况下我们都应该避免使用 eval 函数。99.9% 使用 eval 的场景都有不使用 eval 的解决方案。
1、伪装的 eval
定时函数 setTimeout 和 setInterval 都可以接受字符串作为它们的第一个参数。 这个字符串总是在全局作用域中执行,因此 eval 在这种情况下没有被直接调用。
2、安全问题
eval 也存在安全问题,因为它会执行任意传给它的代码, 在代码字符串未知或者是来自一个不信任的源时,绝对不要使用 eval 函数。

\

undefined 和 null
undefined 是一个值为 undefined 的类型。

\

这个语言也定义了一个全局变量,它的值是 undefined,这个变量也被称为 undefined。
但是这个变量不是一个常量,也不是一个关键字。这意味着它的值可以轻易被覆盖。

\

访问未修改的全局变量 undefined。
由于没有定义 return 表达式的函数隐式返回。
return 表达式没有显式的返回任何内容。
访问不存在的属性。
函数参数没有被显式的传递值。
任何被设置为 undefined 值的变量。

\

由于全局变量 undefined 只是保存了 undefined 类型实际值的副本, 因此对它赋新值不会改变类型 undefined 的值。
chrome浏览器控制台:window.undefined好像不能被覆盖了

避免覆盖技巧

\

//1、使用一个传递到匿名包装器的额外参数
var undefined = 123;
(function(something, foo, undefined) {
    // 局部作用域里的 undefined 变量重新获得了 `undefined` 值
})('Hello World', 42);

//2、在函数内使用变量声明
var undefined = 123;
(function(something, foo) {
    var undefined;
    ...
})('Hello World', 42);

\

null

\

JavaScript 中的 undefined 的使用场景类似于其它语言中的 null,实际上 JavaScript 中的 null 是另外一种数据类型。
它在 JavaScript 内部有一些使用场景(比如声明原型链的终结 Foo.prototype = null),但是大多数情况下都可以使用 undefined 来代替。

\

自动分号插入
JavaScript 不是一个没有分号的语言,恰恰相反上它需要分号来就解析源代码。 因此 JavaScript 解析器在遇到由于缺少分号导致的解析错误时,会自动在源代码中插入分号。——可能会产生一些不希望的副作用

\

var foo = function() {
} // 解析错误,分号丢失
test()

\

在前置括号的情况下,解析器不会自动插入分号

\

log('testing!')
(options.list || []).forEach(function(i) {})

//上面代码被解析器转换为一行。
log('testing!')(options.list || []).forEach(function(i) {})

\

log 函数的执行结果极大可能不是函数;这种情况下就会出现 TypeError 的错误,详细错误信息可能是 undefined is not a function。

\

建议绝对不要省略分号,同时也提倡将花括号和相应的表达式放在一行, 对于只有一行代码的 if 或者 else 表达式,也不应该省略花括号。

\

其他

\

setTimeout 和 setInterval
作为第一个参数的函数将会在全局作用域中执行,因此函数内的 this 将会指向这个全局对象。

\

function Foo() {
    this.value = 42;
    this.method = function() {
        // this 指向全局对象
        console.log(this.value); // 输出:undefined
    };
    setTimeout(this.method, 500);
}
new Foo();

\

setInterval:当回调函数的执行被阻塞时,setInterval 仍然会发布更多的回调指令。在很小的定时间隔情况下,这会导致回调函数被堆积起来。

\

function foo(){
    // 阻塞执行 1 秒
}
setInterval(foo, 100);

\

上面代码中,foo 会执行一次随后被阻塞了一秒钟。
在 foo 被阻塞的时候,setInterval 仍然在组织将来对回调函数的调用。 因此,当第一次 foo 函数调用结束时,已经有 10 次函数调用在等待执行。

\

处理可能的阻塞调用:使用setTimeout

\

function foo(){
    // 阻塞执行 1 秒
    setTimeout(foo, 100);
}
foo();

\

不仅封装了 setTimeout 回调函数,而且阻止了调用指令的堆积,可以有更多的控制。 foo 函数现在可以控制是否继续执行还是终止执行。

\

清除所有定时器:由于没有内置的清除所有定时器的方法,可以采用一种暴力的方式来达到这一目的。

\

// 清空"所有"的定时器
for(var i = 1; i < 1000; i++) {
    clearTimeout(i);
}
// 如果定时器调用时返回的 ID 值大于 1000不会被清除
// 因此我们可以事先保存所有的定时器 ID,然后一把清除。

\

setTimeout 和 setInterval 也接受第一个参数为字符串的情况。 这个特性绝对不要使用,因为它在内部使用了 eval。

\

function foo() {
    // 将会被调用
}

function bar() {
    function foo() {
        // 不会被调用
    }
    setTimeout('foo()', 1000);
}
bar();

\

由于 eval 在这种情况下不是被直接调用,因此传递到 setTimeout 的字符串会自全局作用域中执行; 因此,上面的回调函数使用的不是定义在 bar 作用域中的局部变量 foo。

\

建议不要在调用定时器函数时,为了向回调函数传递参数而使用字符串的形式。

\

function foo(a, b, c) {}

// 不要这样做
setTimeout('foo(1,2, 3)', 1000)

// 可以使用匿名函数完成相同功能
setTimeout(function() {
    foo(1, 2, 3);
}, 1000)

\

绝对不要使用字符串作为 setTimeout 或者 setInterval 的第一个参数, 这么写的代码明显质量很差。当需要向回调函数传递参数时,可以创建一个匿名函数,在函数内执行真实的回调函数。

\

原文地址:JavaScript 秘密花园