你不知道的javascript(上卷)-学习笔记

作用域和闭包

作用域是什么

  • 存储变量并可方便找到这些变量的规则

  • js是一门编译语言

    • 任何js代码片段在执行前都要进行编译,做好执行的转变,并且通常马上就会执行它
  • js工作原理

    • 角色

      • 引擎

        • 从头到尾负责整个js程序的编译和执行过程
      • 编译器

        • 负责语法分析及代码生成等脏活累活
      • 作用域

        • 负责收集并维护所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
    • 变量的赋值操作会执行两个动作

      • 首先编译器会在当前作用域中声明一个变量(如之前没有声明过)
      • 然后,在运行时,引擎会在作用域中查找该变量,如果能找到就会对它进行赋值
    • 实例:var a=2;

  • 遍历嵌套作用域链的规则

    • 引擎从当前的执行作用域开始查找变量,如何找不到就会向上一层继续查找。当抵达最外层的全局作用域时,无论能否找到,查找过程都会停止
  • 异常

    • ReferenceError

      • 同正常模式,或者说宽松/懒惰模式相比,严格模式在行为上有很多不同。

        • 当引擎执行LHS时,如果找不到,会创建一个全局变量
        • 当引擎执行RHS时,如果找不到,会抛出ReferenceError
      • 严格模式,禁止自动或隐式地创建全局变量

      • 代表作用域判别失败

    • TypeError

      • 当尝试对变量进行不合理的操作时

        • 对非函数类型的值进行函数调用
        • 引用null或者undefined类型的值中的属性
      • 作用域判别成功,但是操作非法或不合理

  • LHS

    • 查找的目的是赋值
  • RHS

    • 查找的目标是获取变量的值

词法作用域

  • 定义

    • 是写代码时,将变量和块作用域写在哪里来决定的
    • 当使用词法分析器处理代码时,会保持作用域不变(大部分情况下如此)
  • 查找

    • 作用域查找会在找到第一个匹配的标识符时停止
    • 又称“遮蔽效应”
    • 作用域查找始终从运行时所处的最内部的作用域开始,逐级向外或者说向上进行,直到遇到第一个匹配的标识符为止
  • 欺骗词法

    • 在运行时修改代码,修改词法作用域
    • eval
function fool(str, a){
  eval(str);
  console.log(a, b);
}
var b = 2;
fool("var b= 3l", 1);//1,3
	- 在严格模式中,eval()在运行时有自己的词法作用域,意味着其中的声明无法修改所在的作用域
	- 类似方法

		- setTimeout()
		- setInterval()
		- new Function()

- with
function fool(obj){
  with(obj){
   a= 2;
   }
}

var o1={a:3};
var o2={b:3};

fool(o1);
console.log(o1.a);//2

fool(o2);
console.log(02.a);//undefined
console.log(a)//2 ,a被泄漏到全局作用域了
- o2的作用域、fool()的作用域、全局作用域中都没找到a,因此当执行a=2时,自动创建了一个全局变量(非严格模式)

	- with块可以将一个对象处理为词法作用域,但是内部正常的var声明不会被限制在这个块作用域中,而是被添加到with所处函数的作用域中
	- with实际上根据你传递给它的对象凭空创建了一个全新的词法作用域

- 对性能的影响

	- js的性能优化

		- 引擎在编译阶段会进行性能优化,有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行中快速找到标识符。
		- 引擎如果发现eval/with代码,它只能简单假设关于标识符位置的判断都是无效,因而取消优化,导致运行变慢

函数作用域和块作用域

  • 函数作用域

    • 属于这个函数的全部变量都可以在整个函数范围内使用及复用

    • 在嵌套的作用域中也可使用

    • 规避冲突的方式

      • 命名空间

        • 第三方库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象。这个对象被称为命名空间
        • 所有需要暴露给外界的功能都会成为这个对象都属性
      • 模块管理

        • 通过依赖管理器机制,将库的标识符显式地导入到另外一个特定的作用域中
  • 最小特权原则/最小授权/最小暴露原则

    • 应该最小限度地暴露必要内容,从而使其他内容都“隐藏”起来
  • 函数声明和函数表达式

    • 区别是它们的名称标识符将会绑定在何处
    • 如果function是声明中的第一个词,则是函数声明,否则是函数表达式
    • (function fool(){…}) 作为函数表达式意味着fool只能在…所代表的位置被访问,外部作用域则不行。fool变量名被隐藏在自身中,意味着不会非必要的污染外部作用域
  • 立即执行函数表达式-IIFE

    • var a = 2;

(function fool(){
var a = 3;
console.log(a);//3
})();

console.log(a);//2
- 函数名不是必要的
- 另外的形式

	- (function () {...}())
  • 块作用域

    • var提升

      • 当使用var声明变量时,它写在哪里都是一样的,最终都会属于外部作用域

      • 提升

        • 声明会被视为存在于其所出现的作用域的整个范围内
    • with

      • 用with从对象中创建出的作用域仅在with声明中而非外部作用域生效
    • try/catch

      • catch分句会创建一个块作用域,其中声明的变量仅在catch内部生效
    • let

      • 将变量绑定到所在的任意区块中
      • let为其声明的变量隐式地劫持了所在的块作用域
      • let声明的变量不会被提升。代码被运行前,声明并不存在
      • ES6引入
    • const

      • 用来创建块作用域,但是其值是固定的(常量)。声明后不可修改。ReferenceError
    • 函数不是块作用域的唯一单元。块作用域指的是变量和函数不仅可以属于所处的区域,也可以属于某个代码块(通常指{}内部)

提升

  • 正确的思考思路

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

    • 提升

      • 变量和函数的声明会被从它们声明的位置被“移动”到最上面,最先开始执行
      • 先有声明再有赋值
      • 只有声明本身会被提升,而赋值或其他运行逻辑会留在原地
      • 函数声明会被提升,但是函数表达式不会被提升
      • 函数优先被提升,之后是变量
      • 重复的声明会被忽略,后面出现的函数声明可以覆盖前面的

作用域闭包

  • 闭包

    • 是基于词法作用域书写代码时所产生的自然结果
    • 当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行
    • function foo(){
      var a=2;

    function bar(){
    console.log(a);
    }

return bar;//return
}

var baz = foo();
baz();//2-这就是闭包的效果

	- bar()的词法作用域能访问foo()的内部作用域
	- 将bar作为值类型进行传递。bar所引用的函数对象本身作为返回值
	- 在foo()执行后,其返回值赋值为baz,并调用baz(),实际上是通过不同的标识符引用内部的函数bar()
	- bar()可以被正常执行。它在自己定义的词法作用域之外执行
	- 在foo()执行后,通常会期待foo()整个内部作用域都被销毁,因为引擎有垃圾回收器来释放不再使用的内存空间。但是闭包的存在,阻止被回收。内部作用域仍存在
	- bar()依然持有对该作用域的引用,这个引用就叫闭包

- 闭包使得函数可以继续访问定义时的词法作用域
- 无论使用何种手段将内部作用域传递到所在的词法作用域之外,它都会持有对原始定义的作用域的引用,无论在何时执行此函数都会使用闭包
- 如果将函数作为值类型进行传递,就是闭包的应用

	- 如回调函数
  • 循环和闭包

    • for(var i=1;i<=5;i++){
      (function(j){
      setTimeout(function timer(){
      console.log(j);
      }, j*1000);
      })(i);
      }
    • 在迭代内使用IIFE会为每个迭代的都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代内部都会含有一个正确值的变量
    • for循环头部的let声明,在每次迭代时都会声明。随后的每一个迭代都会使用上一个迭代结束时的值来初始化这个变量
  • 模块

    • 模块模式的2个必要条件

      • 1.必须有外部的封闭函数,该函数必须至少被调用一次,每次调用都会创建一个新的模块实例

      • 2.封闭函数必须至少有一个内部函数,这样内部函数才能在私有作用域中形成闭包,并可以访问或者修改私有的状态

      • 实例

        • function CoolModule(){
          var something = “cool”;

    function doSomething(){
    console.log(something);
    }

    return {
    doSomething: doSomething
    };

}

var foo = CoolModule();
fool.doSomething();//cool

- ES6模块机制

	- 通过模块系统加载时,ES6会将文件作为独立的模块来处理,每个模块都可以导入其他模块或者特定的API成员(import),也可以导出自己的API成员(export)
	- import

		- 可以将一个模块中的一个或者多个API导入到当前作用域中,并分别绑定在一个变量上
		- import hello from "bar";

	- module

		- 将整个模块的API导入并绑定在一个变量上
		- module foo from "foo";

	- export

		- 将当前模块的一个标识符导出为公共API
		- function awesome(){

}

export awesome;

附录

  • 动态作用域

    • js不具有动态作用域
    • this机制像是动态作用域
    • 词法作用域是在写代码或者说定义时确定的
    • 动态作用域是在运行时确定的
  • 箭头函数在涉及到this绑定时,放弃了普通this的绑定规则,用当前的词法作用域覆盖this本来的值

this和对象原型

this入门

  • 提供了一种优雅的方式来隐式传递一个对象的引用
  • function identify(){
    return this.name.toUpperCase();
    }

function speak(){
var greeting = "hello, I’m "+identify.call(this);
console.log(greeting);
}

var me = {name : “Kyle”}
var you = {name: "Reader}

identify.call(me);//KYLE
identify.call(you);//READER

speak.call(me);//hello, I’m KYLE;
speak.call(you);//hello,I’m READER;

  • this在任何时候都不指向函数的词法作用域
  • this是在运行时绑定的,上下文取决于函数调用时的各种条件。this之取决于函数的调用方式
  • 当函数被调用时,会创建一个活动记录(执行上下文),这个记录会包括函数的调用栈和调用方式,传入的信息等

this全面解析

  • 调用位置

    • 调用位置是函数在代码中被调用的位置
    • 分析调用栈,我们寻找的调用位置就在当前正在执行函数的前一个调用中
    • 使用开发者工具中,调用栈的第二个元素,是真正的调用位置
  • 绑定规则

    • 分类

      • 默认绑定

        • this指向全局对象
        • function foo(){
          console.log(this.a);
          }

var a=2;
foo();//2
- 严格模式下,this会绑定到undefined

	- 隐式绑定

		- 调用位置是否有上下文,是否被某个对象拥有或包含
		- function foo(){

console.log(this.a);
}

var obj ={
a:2,
foo: foo
}

obj.foo();//2
- 对象属性引用链上只有上一层或者说最后一层在调用位置起作用

			- function foo(){

console.log(this.a);
}

var obj2 ={
a: 42,
foo: foo
}

var obj1 = {
a: 2,
obj2: obj2
}

obj1.obj2.foo();//42

		- 隐式丢失

			- 被隐式绑定的函数丢失绑定对象,会应用默认绑定。全局变量或undefined
			- 参数传递是一种隐式赋值
			- 回调函数丢失this绑定是非常常见的

				- function foo(){

console.log(this.a);
}

var obj = {
a: 2,
foo: foo
}

var a = “oops”;

setTimeout(obj.foo, 100);//oops

	- 显式绑定

		- 使用call() apply()方法

			- function foo(){

console.log(this.a);
}

var obj ={a:2};
foo.call(obj);//2
- 将foo的this绑定在obj上

		- 硬绑定

			- bind()
			- function foo(something){

console.log(this.a, something);
return this.a + something;
}

var obj = { a:2};
var bar = foo.bind(obj);
var b = bar(3);//2 3
console.log(b);//5
- bind制定的参数设置为this的上下文(obj),并调用原始函数

	- new绑定

		- 不存在构造函数,只存在对于函数的构造调用
		- 使用new来调用函数时,执行的过程

			- 1.创建或者构造一个全新的对象
			- 2.新的对象被执行[[Prototype]]连接
			- 3.新的对象会绑定到函数调用的this
			- 4.如何函数没有返回其他对象,new表达式中的函数调用会自动返回这个新的对象

- 规则的判定顺序

	- 1.函数是否在new中调用,new
	- 2.是否通过call/apply显式绑定,显式绑定
	- 3.是否隐式绑定,上下文对象
	- 4.默认绑定

- 绑定意外

	- 把null或者undefined作为this的绑定对象,传入call/apply/bind,会作为默认绑定规则
	- 软绑定

		- 给默认绑定指定一个全局对象和undefined之外的值,可以实现和硬绑定相同的效果,同时保留隐式绑定或显式绑定修改this的能力
		- softBind
		- if(!Function.prototype.softBind){

Function.prototype.softBind = function(obj){
var fn = this;
var curried = [].slice.call(arguments, 1);
var bound = function(){
return fn.apply(
(!this || this === (windows || global)) ?
obj: this,
curried.concat.apply(curried, arguments);
);
};

bound.prototype = Object.create(fn.prototype);

return bound;
};

}

- this词法

	- 箭头函数不使用this的四种规则,而是根据外层作用域来决定this

对象

  • 语法

    • var myObj = {
      key: value,
      };

      • 文字语法
    • var myObj = new Object();
      myObj.key = value

      • 构造形式语法
  • 内容

    • 在引擎内部,值一般不会存储在对象容器中,存储在对象容器中的是属性的名称,它们是指向值真正存储位置的引用

    • .操作符

      • 属性的名字满足标识符命名规范
      • 属性访问
    • []操作符

      • 键访问
      • 可以接受任何UTF-8/Unicode字符串作为属性名
    • 函数用于不属于一个对象,仅是对函数对象的引用

    • 复制对象

      • 浅拷贝

        • 复制出的对象会复制属性的,但是不会复制引用的

        • Object.assign()方法实现浅拷贝

          • 第一个参数是目标对象
          • 后面跟一个或多个源对象
          • 遍历一个或多个源对象的所有可枚举类型的自有键,并把它复制到目标对象,返回目标对象
      • delete

        • 用来删除对象的可删除属性
        • 如果对象的某个属性是某个对象/函数的最后一个引用,对这个属性执行删除后,这个未引用的对象/函数就可以被垃圾回收
    • 不变性

      • 对象常量

        • writable:false configurable:false就可以创建一个真正的常量属性
        • var myObject = {};

Object.defineProperty(myObject, “FAVORITE_NUMBER”,{
value: 42,
writable: false,
configurable: false
});

	- 禁止扩展

		- Object.preventExtensions();

	- 密封

		- Object.seal()相当于 调用Object.preventExtensions,同时configurable: false

	- 冻结

		- Object.freeze() 调用seal()方法,并writable:false

- 存在性

	- in

		- 会检查属性是否在对象及其[[Prototype]]原型链上

	- hasOwnProperty()

		- 只会检查属性是否在对象上

	- 枚举

		- 可枚举相当于可以出现在对象属性的遍历中
		- for in 在数组上应用可能出现问题,最好只在对象上使用for in,数组采用传统的for循环来

	- Object.keys()

		- 都只会查找对象直接包含的属性
		- 返回数组,包含所有可枚举的属性

	- Object.getOwnPrototypeNames()

		- 都只会查找对象直接包含的属性
		- 返回数组,包含所有属性

- 遍历

	- forEach

		- 遍历数组中的所有值,并忽略回调函数的返回值

	- every

		- 会一直运行到回调函数返回false或者假

	- some

		- 会一直运行到回调函数返回true或者返回真

	- for in

		- 无法直接获取属性值,实际上遍历的是对象中的所有可枚举属性,需要收到获取属性

	- for of

		- 会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象中的next()方法来遍历所有返回值
		- 普通的对象没有内置的@@iterator
		- var myArray = [1,2,3];

for (var v of myArray){
console.log(v);
}
//1
//2
//3

  • 类理论

    • 代码的/继承描述了一种代码的组织结构形式,一种建模方法

    • js中的父类和子类的关系只存在于两者构造函数对应的.prototype对象中,它们的构造方法不存在直接联系

    • js不提供多重继承

    • 显式混入mixin()

      • function mixin (sourceObj, targetObj){
        for( var key in souceObj){
        if(!(key in targetObj)){
        targetObj[key] = sourceObj[key];
        }
        }
        return targetObj;
        }

var Vehicle = {
engines : 1,
ignition: function(){
console.log(“Turnning 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!”);
}
});

- 类是一种设计模式

原型

  • [[Prototype]]

    • 是特殊的内置属性

    • 是对其他对象的引用

    • 普通的原型链最终都会指向内置的Object.prototype

    • 属性的设置

      • myObject.foo = “bar”;

        • 如果myObject对象中包含foo属性,则这条语句只会修改已有属性的取值

        • 如果myObject不包含foo属性,原型链会被遍历,如果原型链中找不到foo,foo会被添加到myObject上

        • 如果foo既存在的myObject上,也出现在原型链上层,则会发生屏蔽,会选择原型链最底层的foo属性

        • 如果foo存在于原型链上,

          • 该属性未被标记为只读,则会在myObject添加一个foo属性
          • 该属性被标记为只读,无法修改属性或者在myObject创建属性,严格模式会抛出错误,非严格模式该语句被忽略
          • 且它是一个setter,会调用这个setter
    • js中只有对象

    • 所有函数都默认拥有一个名为prototype的公有且不可枚举的属性

    • new Foo()并不会创建关联

    • js会在两个对象直接建立关联,这样一个对象就可以通过委托访问另一个对象的属性和函数

    • 检查类关系

      • 内省

        • 检查一个实例的继承祖先的过程

        • instanceof

          • a instanceof Foo
          • 在a的整条原型链中是否有指向Foo.prototype的对象
        • isPrototypeOf

          • Foo.prototype.isPrototypeOf a
          • 在a的整条原型链中是否出现过Foo.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,这样就可以充分发挥委托的威力

行为委托

  • 面向委托的设计

    • 委托的理论

      • 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);
- 对象之间是兄弟关系,不是父子关系

附录

  • polyfill

    • 用于兼容旧版本的浏览器
    • 使用polyfill代码在浏览器中实现新的功能
  • 空对象

    • Object.create(null)
    • 不会创建Object.prototype这个委托

XMind: ZEN - Trial Version

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值