Skip to content

你不知道的javaScript(一) #16

Description

@Lindysen

# 第一部分 作用域和闭包

程序中的一段代码经过词法分析-->语法分析-->代码生成三个阶段。

词法分析,把由字符组成的字符串拆分成词法单元(token)

语法分析,把词法单元流组成元素逐级嵌套的树,抽象语法树

代码生成,把抽象语法树变成可执行代码

对于js代码来说,是在执行前进行编译的。

引擎:从头到尾负责js代码的执行

编译器:负责词法分析和代码生成阶段

作用域:负责维护与查找标识符

作用域嵌套

在当前作用域没找到标识符时,引擎就会逐级向外层作用域进行查找,直到找到为止或抵达最外层作用域为止(全局作用域)

在非严格模式下,若在全局作用域未找到目标变量,全局作用域就会创建一个该名称的变量并返回给引擎。

词法作用域由你写代码时将变量或函数写在哪里决定的,因此词法分析器处理代码时保持作用域不变。换句话说,就是你的函数在哪里调用,如何调用,它的词法作用域只由函数被声明时所处的位置决定。编译的词法分析阶段基本知道全部标识符在哪里以及如何声明的。从而能预测执行过程中如何对它进行查找

词法作用域就是一套关于引擎如何寻找变量以及在何处找出变量的规则,词法作用域最重要的特征是它的定义过程发生在书写阶段。

函数作用域与块作用域

函数作用域
是指属于这个函数的全部变量在整个函数中都可以使用,包括在嵌套作用域中。任意一段代码,用函数声明封装起来,可以将内部的变量函数隐藏起来,外部作用域无法访问。

函数表达式

(function foo(){

})()

函数表达式和函数声明的最重要的区别在于名称标识符绑定在何处。函数声明被绑定在所在作用域中。函数表达式被绑定在函数表达式自身的函数中。

匿名函数表达式

setTimeOut(function() {
    
},100)

匿名函数书写起来便捷但是有几个缺点
1.匿名函数在堆栈中不会显示有意义的函数,不方便调试
2. 匿名函数不方便在函数内部调用自身,只能使用过时的arguments.callee引用
3. 匿名函数省略了对于代码可读性/理解性很重要的函数名

立即执行函数

(function foo() {
    
})()

函数被包含在一对()内部,成为一个表达式,通过末尾添加上()可以立即执行该函数表达式。

块作用域
变量的声明距离使用的地方越近越好,最大限度的的本地化。

  1. try/catch,catch分句会创建一个块作用域,其中声明的变量仅在catch内部中使用。
  2. let关键字可以将变量绑在任意作用域中,通常是({...})中,换句话说,let为其声明的变量隐式的绑定块作用域
  3. const可以创建块作用域但其值是固定的之后尝试修改其值将会报错。
  4. 暂存性死区

变量提升
引擎在解释js代码之前会先进行编译,编译的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。
正确的思路是 变量和函数在内的声明都会在代码被执行前先被处理。

var a = 2;js实际上会将其看成2个声明;第一个var a 第二个 a = 2; 第一个声明在编译阶段,第二个赋值声明在执行阶段。

函数和变量声明被提到作用域的最上面的过程称为提升。换句话说先有声明再有赋值。只有声明语句被提升,而赋值等其他运行逻辑会留在原地。
每个作用域都存在提升。函数声明会提升但是函数表达式却不会提升。即使是具名的函数表达式,名称标识符在赋值操作前也不能使用

函数声明和变量声明都会被提升。但是值得注意的一个细节是函数首先被提升然后才是变量。

作用域闭包

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前作用域之外执行。
无论通过什么方式将内部函数传递到所在的词法作用域之外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

模块有两个特征:

  1. 为创建内部作用域而调用一个包装函数
  2. 保障函数的返回值必须至少包括一个对内部函数的引用

# 第二部分 this和对象原型

this 在任何时候都不会指向函数的词法作用域。在javaScript内部,作用域确实和对象一样,可见标识符相当于作用域的属性,但是作用域“对象”无法通过js代码访问,它存在与js引擎内部

this不指向函数本身和词法作用域

this是运行时绑定的并不是编写时绑定。this的值取决于函数调用的各种条件,和函数声明的位置没有关系,只取决于函数调用关系。

当函数被调用时 会创建一个活动记录,记录函数在哪里被调用(调用栈),函数调用方式,函数传入的参数,this只是记录的一个属性,会在函数的执行过程中用到

调用位置(函数被调用的方法):函数在代码中被调用的位置。

调用栈:为了到达当前执行位置所调用的所有函数

在函数的执行过程中调用位置如何决定this的绑定对象。

绑定规则:

  1. 默认绑定
    最常用的函数调用类型:独立函数调用。this 绑定到全局对象或者 undefined 上,取决于是否是严格模式。在严格模式下绑定到undefined,否则绑定到全局对象。

  2. 隐式绑定
    调用位置是否存在上下文对象。当函数引用由上下文对象时,隐式绑定规则函数调用中的 this 绑定到这个上下文对象。

隐式丢失

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(); //  bar 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
  1. 显式绑定,使用call(...)和apply(...),它们的第一个参数是一个对象,它们会把这个对象绑定到 this,接着在调用函数时指定这个 this。因为你可以直接指定 this 的绑定对象。

  2. new绑定。在javaScript中,构造函数只是一些使用new操作符时被调用的函数,他们并不属于哪个类也不会实例化哪个类,实际上, 它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已。
    实际上不存在什么构造函数,只有函数的构造调用。

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

    1. 创建(或者说构造)一个全新的对象。
    2. 这个新对象会被执行[[原型]]连接。
    3. 这个新对象会绑定到函数调用的this。
    4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
  3. 箭头函数,箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决 定 this。箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这 其实和 ES6 之前代码中的 self = this 机制一样。

function foo() {
   // 返回一个箭头函数
    return (a) => {
    //this 继承自 foo()
    console.log( this.a ); };
}
var obj1 = { a:2 };
var obj2 = { a:3 };
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是 3 !

// foo() 内部创建的箭头函数会捕获调用时 foo() 的 this。由于 foo() 的 this 绑定到 obj1, bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。(new 也不 行!)

对象

对象可以通过文字形式和构造形式定义

文字形式可以一次定义多个属性,但是构造形式只能一个个添加

var myObj = { key: value
// ... };

var myObj = new Object(); myObj.key = value;

类型
在Js中有7个主要类型

  • string
  • boolean
  • number
  • null
  • undefind
  • symbol
  • object

js 内置对象

  • String
  • Number
  • Array
  • Function
  • Date
  • Error
  • RegExp
  • Object
    null undefind没有对应的构造形式,他们只有文字形式。

属性描述符
从ES5开始,所有的属性都具备了属性描述符

var myObject = { 
    a:2
};

Object.getOwnPropertyDescription(myObject,'a');
<!--{-->
<!--    value: 2,-->
<!--    configurable:true,-->
<!--    writeable:true,-->
<!--    enumerable:true,-->
<!--}-->

 Object.defineProperty( myObject, "b", {
         value: 2,
         writable: true,  //决定是否可以修改属性值
         configurable: true,//可配置的,只要属性是可配置的就可以通过Object.defineProperty修改特性值,禁止删除这个属性
         enumerable: true// 是否出现在对象属性枚举中
     } );

结合write:false和configurable:false来创建一个真正的常量属性(不可修改、 重定义或者删除):

禁止扩展

  1. 禁止添加新属性
var myObject = {
     a:2
};
Object.preventExtensions( myObject );
  1. 密封,实际上会在一个现有对象上调用 Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false。但是可修改属性值
Object.seal()

  1. 冻结,实际上会在一个现有对象上调用 Object.seal(..) 并把所有“数据访问”属性标记为 writable:false。
Object.freeze()

对象默认的[[Put]]和[[Get]]操作分别可以控制属性值的设置和获取

当你给一个属性设置getter,setter或者两者都有时,这个属性会被定义为“访问描述符”。

var myObject = {
// 给 a 定义一个 getter 
 get a() {
   return this._a_; 
 },
// 给 a 定义一个 setter 
set a(val) {
  this._a_ = val * 2; 
  }
};
Object.defineProperty( 
    myObject, // 目标对象 
    "b", // 属性名
    {
    // 描述符
    // 给 b 设置一个 getter
    get: function(){ return this.a * 2 },
    // 确保 b 会出现在对象的属性列表中
      enumerable: true
            }
    );

存在性

in操作符会检查属性是否在对象及其[[prototype]]原型链中。hasOwnProperty只会检查属性是否在对象中,不会检查prototype链。

可枚举相当于可以出现在对象的遍历中

var myObject = { };
     Object.defineProperty(
         myObject,
    "a",
    // 让 a 像普通属性一样可以枚举
    { enumerable: true, value: 2 }
);
     Object.defineProperty(
         myObject,
        "b",
        // 让b不可枚举
        { enumerable: false, value: 3 }
);
    myObject.b; // 3
    ("b" in myObject); // true myObject.hasOwnProperty( "b" ); // true
   // .......
    for (var k in myObject) { console.log( k, myObject[k] );
}
// "a" 2

Object.keys(..) 会返回一个数组,包含所有可枚举属性。

for...of首先会向被访问对象请求迭代器对象,然后通过调用迭代器对象的next()方法来遍历所有返回值。如果对象本身定义了迭代器的话也可以遍历对象

和数组不同,普通对象没有内置的@@iterator,所以无法完成for...of循环。当然我们可以给任何想遍历对象添加自定义@@iterator

var myObject = {
    a:2,
    b:3,
};
Object.defineProperty(myObject, Symbol.iterator,{
    configurable: true,
    enumerable: false, // 不可枚举
    writable: false,
    value: function() {
        var o = this;
        var idx = 0;
        var ks = Object.keys( o );
        return {
            next:function() {
                return {
                    value: o[ks[idx++]],
                    done: idx< ks.length,
                }
            }
        }
    }
})

原型

JavaScript中的对象都有一个[[Prototype]]内置属性,其实就是对于其他对象的引用。

Object.create() 会把新对象的[[Prototype]]设置为参数对象。

所有普通[[Prototype]]链的尽头都是Object.prototype

属性屏蔽

如果属性名 foo 既出现在 myObject 中也出现在 myObject 的 [[Prototype]] 链上层,那 么就会发生屏蔽。myObject 中包含的 foo 属性会屏蔽原型链上层的所有 foo 属性,因为 myObject.foo 总是会选择原型链中最底层的 foo 属性。

如果 foo 不直接存在于 myObject 中而是存在于原型链上层时 ==myObject.foo = "bar"== 会出现的三种情况。

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

解释一下第二条

只读属性会阻止[[prototype]]链下层隐式创建同名属性。
,这个限制只存在于 = 赋值中,使用 Object. defineProperty(..) 并不会受到影响。

javaScript中只有对象

所有的函数默认都有一个prototype共有且不可枚举的属性,指向另一个对象。这个对象被称为Foo的原型

function Foo() {
    
}
Foo.prototype {}

 Foo.prototype.constructor === Foo;
 // true
 // Foo.prototype 默认(在代码中第一行声明时!)有一个公有并且不可枚举(参见第 3 章) 的属性 .constructor,这个属性引用的是对象关联的函数(本例中是 Foo)
 
 // 这是一个很不幸的误解。实际上,.constructor 引用同样被委托给了 Foo.prototype,而 Foo.prototype.constructor 默认指向 Foo。

 
 
var a = new Foo();
 a.constructor === Foo; // true
Object.getPrototypeOf(a) === Foo.protoype // true




在面向类的语言中,类可以被复制(或者说实例化)多次,就像用模具制作东西一样,实例化(或者继承)一个类就意味着“把类的 行为复制到物理对象中”,对于每一个新实例来说都会重复这个过程。

但在 JavaScript 中,并没有类似的复制机制。你不能创建一个类的多个实例,只能创建 多个对象,它们 [[Prototype]] 关联的是同一个对象。但是在默认情况下并不会进行复制, 因此这些对象之间并不会完全失去联系,它们是互相关联的。

==调用new Foo()会创建a,其中一步是给a的一个内部的[[prototype]]连接关联到 Foo.prototype 指向的那个对象。==

在javaScript中对于构造函数最准确的解释是,所有带new的函数调用

函数不是构造函数,但是当且仅当使用new时,函数调用就会变成“构造函数调用”

“constructor 并不表示被构造”。

语句 Bar.prototype = Object.create(
Object.create(..) 会凭空创建一个“新”对象并把新对象内部的 [[Prototype]] 关联到你 指定的对象(本例中是 Foo.prototype)。

function Foo(name) { 
    this.name = name;
}
Foo.prototype.myName = function() { 
    return this.name;
};
function Bar(name,label) { 
    Foo.call( this, name ); 
    this.label = label;
}
// 我们创建了一个新的 Bar.prototype 对象并关联到 Foo.prototype Bar.prototype = Object.create( Foo.prototype );
// 注意!现在没有 Bar.prototype.constructor 了 // 如果你需要这个属性的话可能需要手动修复一下它
Bar.prototype.myLabel = function() { 
    return this.label;
};
var a = new Bar( "a", "obj a" );
 a.myName(); // "a"
 a.myLabel(); // "obj a"
 
。这样做唯一的缺点就是需要创建一个新对象然后把旧对象抛弃掉,不能 直接修改已有的默认对象。
ES6 添加了辅助函数 Object.setPrototypeOf(..),可以用标准并且可靠的方法来修 改关联。
// ES6 开始可以直接修改现有的 Bar.prototype Object.setPrototypeOf( Bar.prototype, Foo.prototype );

a instanceof Foo
instanceof 操作符的左操作数是一个普通的对象,右操作数是一个函数。

这个方法只能处理对象(a)和函数(带 .prototype 引用的 Foo)之间的关系

instanceof 回答
的问题是:
==在 a 的整条 [[Prototype]] 链中是否有指向 Foo.prototype 的对象?==

==现在我们知道了,[[Prototype]] 机制就是存在于对象中的一个内部链接,它会引用其他
对象。
通常来说,这个链接的作用是:如果在对象上没有找到需要的属性或者方法引用,引擎就 会继续在 [[Prototype]] 关联的对象上进行查找。同理,如果在后者中也没有找到需要的 引用就会继续查找它的 [[Prototype]],以此类推。这一系列对象的链接被称为“原型链”==。

var foo = {
something: function() {
             console.log( "Tell me something good..." );
         }
};
var bar = Object.create( foo );
bar.something(); // Tell me something good...

Object.create(..) 会创建一个新对象(bar)并把它关联到我们指定的对象(foo),这样 我们就可以充分发挥 [[Prototype]] 机制的威力(委托)并且避免不必要的麻烦(比如使 用 new 的构造函数调用会生成 .prototype 和 .constructor 引用)。

我们的实现遵循的是委托设计模式(参见第 6 章),通过 [[Prototype]] 委托到 anotherObject.cool()。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions