你不知道的JavaScript(上)

前端之家收集整理的这篇文章主要介绍了你不知道的JavaScript(上)前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

第一部分 作用域和闭包

第1章 作用域是什么

LHS:变量出现在左侧,目的是对变量进行赋值

RHS:变量出现在右侧,目的获取变量的值

ReferenceError 同作用域判别失败相关,而 TypeError 则代表作用域判别成功了,但是对结果的操作是非法或不合理的。

第2章 词法作用域

词法作用域就是定义在词法阶段的作用域。词法作用域是由在写代码时,将变量和块作用域写在哪里决定的,因此当词法分析器处理代码时会保持作用域不变。

eval和with会在运行时修改或创建新的作用域。如果代码中大量使用 eval(..) 或 with,那么运行起来一定会变得非常慢。

这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认 为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们。

第3章 函数作用域和块作用域

函数作用域

// 第一段代码
var a = 2;
function foo() {
var a = 3; console.log( a );
} 
foo(); 
console.log( a ); 

// 第二段代码
var a = 2;
(function foo() {
var a = 3; console.log( a );
})();
console.log( a ); // 2

区分函数声明和表达式最简单的方法是看 function 关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果 function 是声明中 的第一个词,那么就是一个函数声明,否则就是一个函数表达式。

比较一下前面两个代码片段。第一个片段中 foo 被绑定在所在作用域中,可以直接通过foo() 来调用它。第二个片段中 foo 被绑定在函数表达式自身的函数中而不是所在作用域中。

换句话说,(function foo(){ .. })作为函数表达式意味着foo只能在..所代表的位置中 被访问,外部作用域则不行。foo 变量名被隐藏在自身中意味着不会非必要地污染外部作用域。

try/catch

try/catch 的 catch 分句会创建一个块作用域,其中声明的变量仅在 catch 内部有效。

try {
   undefined(); // 执行一个非法操作来强制制造一个异常
}
catch (err) {
   console.log( err ); // 能够正常执行! 
}
console.log( err ); // ReferenceError: err not found

let

let 关键字可以将变量绑定到所在的任意作用域中(通常是{...}内部)。换句话说,let为其声明的变量隐式地了所在的块作用域。

var foo = true;
if (foo) {
   let bar = foo * 2;
   bar = something( bar ); console.log( bar );
}
console.log( bar ); // ReferenceError

const

同样可以用来创建块作用域变量,但其值是固定的 (常量)

第4章 提升

回忆一下,引擎会在解释JavaScript 代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的 声明,并用合适的作用域将它们关联起来。

因此,变量和函数在内的所有声明都会在任何代码被执行前首先被处理

a = 2;
var a;
console.log( a );

处理顺序

var a;
a = 2;
console.log( a );
只有声明会被提前,而赋值和其他运行逻辑会留在原地

只有函数声明会被提升,函数表达式不会

foo(); // 不是 ReferenceError,而是 TypeError!
var foo = function bar() { // ...
};

第5章 闭包

function foo() { 
    var a = 2;
    function baz() { 
        console.log( a ); // 2
    }
    bar( baz ); 
 }
function bar(fn) {
    fn(); // 妈妈快看呀,这就是闭包!
}
在执行函数fn时访问到了foo的内部变量a,形成闭包

把内部函数 baz 传递给 bar,当调用这个内部函数时(现在叫作 fn),它涵盖的 foo() 内部
作用域的闭包就可以观察到了,因为它能够访问 a。

闭包的另一种形式:赋值给全局变量

var fn;
function foo() {
var a = 2;
function baz() { console.log( a );
}
fn = baz; // 将 baz 分配给全局变量 }
function bar() {
fn(); // 妈妈快看呀,这就是闭包!
     }
     foo();
     bar(); // 2

另一种形式的闭包

function wait(message) {
         setTimeout( function timer() {
             console.log( message ); // message依然可以用
},1000 ); }
wait( "Hello,closure!" );

wait(..) 执行 1000 毫秒后,它的内部作用域并不会消失,timer 函数依然保有 wait(..)作用域的闭包。

在定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使 用了回调函数,实际上就是在使用闭包!

for (var i=1; i<=5; i++) { 
  (function(j) {
    setTimeout( function timer() { 
        console.log( j ); // 闭包
    },j*1000 );
  })( i );
}

块级作用域

for (var i=1; i<=5; i++) {
    let j = i; // 是的,闭包的块作用域!
    setTimeout( function timer() {
         console.log( j );
    },j*1000 );
}

闭包的使用案例--模块

var foo = (function CoolModule() { 
    var something = "cool";
    var another = [1,2,3];
    function doSomething() { console.log( something );
    }
    function doAnother() {
        console.log( another.join( " ! " ) );
    }
return {
    doSomething: doSomething,doAnother: doAnother
}; })();
 foo.doSomething(); // cool
 foo.doAnother(); // 1 ! 2 ! 3

模块有两个主要特征:(1)为创建内部作用域而调用了一个包装函数;(2)包装函数的返回 值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。

this词法

var obj = {
    id: "awesome",cool: function coolFn() {
        console.log( this.id );
} };
var id = "not awesome"
obj.cool(); // 酷
setTimeout( obj.cool,100 ); // 不酷

cool函数丢失了和this之间的绑定。取一个self=this。或者可以直接用ES6的箭头函数

var obj = { 
    count: 0,cool: function coolFn() {
        // var self = this;
        if (this.count < 1) {
        setTimeout( timer => {
             this.count++;
             console.log( "awesome?" );
         },100 );
    } }
};
obj.cool(); // 酷吧?

或者使用bind(this)

var obj = { 
    count: 0,cool: function coolFn() { if (this.count < 1) {
    setTimeout( function timer(){ 
    this.count++; // this 是安全的 // 因为 bind(..)
    console.log( "more awesome" ); }.bind( this ),100 ); // look,bind()!
    } }
};
obj.cool(); // 更酷了。

第二部分 this和对象原型

第1章 关于this

误解

1.指向自身

this并不是指向函数自身。而是指向函数执行时所在的那个词法作用域。
要想使得this指向函数自身,可以使用foo.call(foo,参数)的形式

2.它的作用域

第二种常见的误解是,this指向函数的作用域

function foo() { 
    var a = 2;
    this.bar(); 
}
function bar() { 
    console.log( this.a );
}
foo(); // ReferenceError: a is not defined

那么this到底是什么?

this是在运行时进行绑定的,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数调用方式。

当一个函数调用时,会创建一个活动记录。这个记录会包含函数在哪里被调用函数调用方法、传参等信息。this就是记录的起重工一个属性

第2章 this全面解析

绑定规则

默认绑定

function foo() { 
    console.log( this.a );
}
var a = 2; 
foo(); // 2

代码中,foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用 默认绑定

值得注意的一点是,如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此 this 会绑定 到 undefined:

function foo() { 
    "use strict";
     console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined

隐式绑定

function foo() { 
    console.log( this.a );
}
var obj = { 
    a: 2,foo: foo 
};
obj.foo(); // 2

几种隐式丢失现象

function foo() { 
    console.log( this.a );
}
var obj = { 
    a: 2,foo: foo 
};
var bar = obj.foo; // 函数别名!
var a = "oops,global"; // a 是全局对象的属性 
bar(); // "oops,global"

虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

function foo() { 
    console.log( this.a );
}
function doFoo(fn) {
    // fn 其实引用的是 foo 
    fn(); // <-- 调用位置!
}
var obj = { 
    a: 2,foo: foo 
};
var a = "oops,global"; // a 是全局对象的属性 doFoo( obj.foo ); // "oops,global"

显示绑定

function foo() { 
    console.log( this.a );
}
var obj = { 
    a:2
 };
 foo.call( obj ); // 2

通过 foo.call(..),我们可以在调用 foo 时强制把它的 this 绑定到 obj 上。

new绑定

使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行[[原型]]连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用自动返回这个新对象。
function foo(a) { 
    this.a = a;
}
var bar = new foo(2); 
console.log( bar.a ); // 2

判断this

  1. 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
    var bar = new foo()
  2. 函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是 指定的对象。
    var bar = foo.call(obj2)
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。
    var bar = obj1.foo()
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到 全局对象。
    var bar = foo()

第3章 对象

基本类型

  • string
  • number
  • boolean
  • null
  • undefined
  • symbol
  • object

前五种是简单基本类型,本身并不是对象

内置对象

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error
var strPrimitive = "I am a string"; 
typeof strPrimitive; // "string" 
strPrimitive instanceof String; // false

console.log( strPrimitive.length ); // 13
console.log( strPrimitive.charAt( 3 ) ); // "m"

使用以上两种方法,我们都可以直接在字符串字面量上访问属性或者方法,之所以可以这 样做,是因为引擎自动把字面量转换成 String 对象,所以可以访问属性方法

var myObject = { a: 2
 };
 myObject.a; // 2
 myObject["a"]; // 2

.a 语法通常被称为“属性访问”,["a"] 语法通常被称为“键访问”。
这两种语法的主要区别在于 . 操作符要求属性名满足标识符的命名规范,而 [".."] 语法 可以接受任意 UTF-8/Unicode 字符串作为属性

属性描述符

  • writable

决定是否可以修改属性的值。如果为false,非严格模式下的修改将失败

  • configurable

只要元素时可配置的,就可以使用defineProperty()来修改属性描述符。configurable为false还会禁止删除这个属性

  • enumerable

控制的是属性是否会出现在对象的属性枚举中,比如for...in循环

不变性

1.对象常量

结合writable:falseconfigurable:false就可以创建一个真正的常量属性

2.禁止扩展

Object.preventExtensions()禁止一个对象添加属性并八六已有属性

3.密封

Object.seal()这个方法实际上会在一个现有对象上调用 Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false

4.冻结
Object.freeze(..) 会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal(..) 并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们的值。

第4章 混合对象“类”

多态并不表示子类和父类有关联,子类得到的只是父类的一份副本。类的继承其实就是复制。

多重集成

A-->B
A-->C
B-->D
C-->D

如果A有drive方法并且B和C都重写了这个方法,那么D引用时选择哪个版本呢?

Javascript并不提供“多重集成”功能

混入

显示混入

由于 JavaScript 不会自动实现 Vehicle 到 Car 的复制行为,所以我们需要手动实现复制功能

// 非常简单的 mixin(..) 例子 :
function mixin( sourceObj,targetObj ) {
for (var key in sourceObj) {
// 只会在不存在的情况下复制 
    if (!(key in targetObj)) {
        targetObj[key] = sourceObj[key];
     }
}
return targetObj; 
}

var Vehicle = {
engines: 1,ignition: function() {
console.log( "Turning on my engine." );
},drive: function() {
this.ignition();
console.log( "Steering and moving forward!" );
}
};
var Car = mixin( Vehicle,{
wheels: 4,drive: function() {
Vehicle.drive.call( this );
console.log(
"Rolling on all " + this.wheels + " wheels!");
}
} );

隐式混入
var Something = { 
    cool: function() {
        this.greeting = "Hello World";
        this.count = this.count ? this.count + 1 : 1; }
};

Something.cool();
Something.greeting; // "Hello World"
Something.count; // 1

var Another = {
cool: function() {
// 隐式把 Something 混入 Another
Something.cool.call( this ); }
};
Another.cool();
Another.greeting; // "Hello World"
Another.count; // 1(count 不是共享状态)

小结

类是一种设计模式。许多语言提供了对于面向类软件设计的原生语法。JavaScript 也有类似的语法,但是和其他语言中的类完全不同。

类意味着复制。

混入模式(无论显式还是隐式)可以用来模拟类的复制行为,但是通常会产生丑陋并且脆 弱的语法,比如显式伪多态(OtherObj.methodName.call(this,...)),这会让代码更加难 懂并且难以维护。

第5章 原型

[[prototype]]

相当于隐式原型__proto__

属性设置和屏蔽

myObject.foo = "bar";
  1. 如果在[[Prototype]]链上层存在名为foo的普通数据访问属性(参见第3章)并且没 有被标记为只读(writable:false),那就会直接在 myObject 中添加一个名为 foo 的新 属性,它是屏蔽属性
  2. 如果在[[Prototype]]链上层存在foo,但是它被标记为只读(writable:false),那么 无法修改已有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下,代码会 抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽
  3. 如果在[[Prototype]]链上层存在foo并且它是一个setter(参见第3章),那就一定会 调用这个 setter。foo 不会被添加到(或者说屏蔽于)myObject,也不会重新定义 foo 这 个 setter。
var anotherObject = { a:2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2
myObject.a; // 2

anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false

myObject.a++; // 隐式屏蔽!

anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // true

Object.create()的polyfill代码

if (!Object.create) { 
    Object.create = function(o) {
        function F(){} 
        F.prototype = o;
        return new F();
    };
}

第6章 行为委托

类理论

假设我们需要在软件中建模一些类似的任务(“XYZ”、“ABC”等)。

如果使用类,那设计方法可能是这样的:定义一个通用父(基)类,可以将其命名为 Task,在 Task 类中定义所有任务都有的行为。接着定义子类 XYZ 和 ABC,它们都继承自 Task 并且会添加一些特殊的行为来处理对应的任务。

非常重要的是,类设计模式鼓励你在继承时使用方法重写(和多态),比如说在 XYZ 任务 中重写 Task 中定义的一些通用方法,甚至在添加新行为时通过 super 调用这个方法的原始 版本。你会发现许多行为可以先“抽象”到父类然后再用子类进行特殊化(重写)。

代码

class Task { 
    id;
    // 构造函数 Task()
    Task(ID) { id = ID; } 
    outputTask() { output( id ); }
}

class XYZ inherits Task {
label;
// 构造函数 XYZ()
XYZ(ID,Label) { super( ID ); label = Label; } outputTask() { super(); output( label ); }
}

class ABC inherits Task {
// ...
}

委托理论

首先你会定义一个名为 Task 的对象(和许多 JavaScript 开发者告诉你的不同,它既不是类 也不是函数),它会包含所有任务都可以使用(写作使用,读作委托)的具体行为。接着, 对于每个任务(“XYZ”、“ABC”)你都会定义一个对象来存储对应的数据和行为。你会把 特定的任务对象都关联到 Task 功能对象上,让它们在需要的时候可以进行委托。

Task = {
    setID: function(ID) { this.id = ID; },outputID: function() { console.log( this.id ); }
};

// 让XYZ委托Task
XYZ = Object.create( Task );
XYZ.prepareTask = function(ID,Label) {
this.setID( ID );
this.label = Label;
};
XYZ.outputTaskDetails = function() {
this.outputID();
console.log( this.label );
};
// ABC = Object.create( Task );
// ABC ... = ...

在这段代码中,Task 和 XYZ 并不是类(或者函数),它们是对象。XYZ 通过Object. create(..) 创建,它的 [[Prototype]] 委托了 Task 对象。

委托行为意味着某些对象(XYZ)在找不到属性或者方法引用时会把这个请求委托给另一个对象(Task)。

小结

行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类的关系。JavaScript 的 [[Prototype]] 机制本质上就是行为委托机制。

原文链接:https://www.f2er.com/js/415896.html

猜你在找的JavaScript相关文章