你不知道的JavaScript上卷学习笔记

编译原理
  • 虽然js被称为动态或者解释执行语言,但是实际上它是一门编译语言,虽然不会提前编译,编译结果也不会在分布式系统中移植。
  • 可以把编译过程当做由三部分配合完成:引擎(负责整个编译流程)、编译器(负责语法分析和代码生成等)、作用域(收集和维护所有声明的变量的查询)。
  • 引擎的查找分为:LHS(赋值操作的左侧查找,比如要使用的变量是否被声明了,对变量赋值)、RHS(赋值操作的右侧查找,比如查找变量的源头,使用变量的值)
  • LHS会出现的异常错误:严格模式会抛出ReferenceError(变量不存在/未声明);RHS会出现的异常错误:ReferenceError(变量不存在/未声明)、TypeError(对变量的值的操作不合法)
作用域
  • 词法作用域大多数情况下是不变的,除非使用eval或者with欺骗词法分析器
  • eval:对包含一个或者多个变量声明的“代码”的字符串进行解释,从而破坏原来的词法作用域
  • with:本质上是将一个对象引用作为一个作用域,其属性作为作用域的标识符,从而创建了新的词法作用域
  • 在严格模式下with被完全禁止,eval的间接或者不安全的使用被禁止;在非严格模式下,with会导致变量泄露,比如如下代码:
function foo(obj){
	with(
		obj.a = 2;
	)
}
var o = {};
foo(o);
console.log(o.a);//undefined
console.log(a);//2,非严格模式下a被泄露到全局作用域了
//这是因为with在o的作用域和全局作用域里面没有找到对应的标识符,所以它新建了一个。
  • eval和with都会降低浏览器的处理性能,因为浏览器引擎在对代码进行词法化时,无法确定准确的词法作用域,所以就无法在编译时对作用域查找进行优化。不要使用。
函数作用域和块级作用域
  • 立刻执行函数表达式:( function foo(){ … } )(),第二个括号表意为立刻执行,可以利用这个特性来实现函数的按序执行,例如:
var a = 3;
(function first(second){
	second(window);
})(function second(global){//被当做参数传给执行函数
	var a = 2;
	console.log(a);//2
	console.log(global.a);//3
})
  • 块级作用域:with、try-catch语句中的catch部分,es6的let、const关键字,let应该显示的声明块级作用域,如:
var a = 2;
{
	let a = 3//表意为let声明的变量只在包围它的“{}”作用域内能被调用,这样写也方便内存垃圾回收
}
console.log(a);//2
  • 匿名函数的缺点
    • 匿名函数在栈追踪中不会显示有意义的函数名,使得调试变得困难
    • 函数引用自身只能用arguments.callee引用
    • 缺乏了函数名称的可读性和可理解性
提升
  • 浏览器引擎会在解释JavaScript代码之前先对其进行编译,而编译的一部分工作便是找出所有的声明,并用合适的作用域将其关联起来。
  • 用var声明的变量以及函数声明会被提升到其所处作用域的最前端执行,且函数声明优先级比变量声明要高,例如
console.log(a);//undefined,已声明但未定义
var a = 2;

等价于

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

函数声明同样也会提升,而且优先级比变量声明要高,例如:

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

相当于

function foo(){
	console.log(2);
}
var foo;//虽然变量声明了,但是函数声明的优先级更高,所以被覆盖了
foo();
foo = function(){
	console.log(3);
}
  • 变量和函数的声明会提升,而其赋值和逻辑操作仍然在原来的位置执行
  • let 和 const 声明的变量不会提升
作用域闭包
  • 闭包概念:使函数在当前词法作用域外执行(能记住并访问自己的词法作用域),就产生了闭包
  • 实际会遇上的问题
for(var i = 0; i < 5; i++){//想要实现5秒内每秒输出一个递增的数,实际每秒输出5
	setTiomout(function timer(){
		console.log(i);
	}, i*1000);
}

上面的代码之所以不能达到我们预期的效果,是因为所有的 timer() 共享了同一个词法作用域,即包含变量 i 的作用域,所以输出的 i 的值都是同一个状态下的 i,即循环结束条件 i = 5。当然,最根本的原因是函数的执行顺序:会先执行当前所有非挂载函数,然后再执行挂载队列里的函数(比如这里的 timer() 函数)。可以通过为每个回调函数创建一个包含特定 i 的值私有的作用域,来解决这个问题:

for(var i = 0; i < 5; i++){
	(function(j){//这里使用了IIFE(立刻执行函数),来为每个timer()创建私有的作用域
		setTiomout(function timer(){
		console.log(j);
		}, j*1000);
	})(i)
}

当然我们也可以用es6的 let 关键字来很简单的实现上面的功能:

for(let i = 0; i < 5; i++){//let每次迭代都会重新声明,并且以上一次迭代的值来初始化这个变量
	setTiomout(function timer(){
		console.log(i);
		}, i*1000);
}
  • 模块:
    • 封装函数,返回一个暴露的对象或者函数,例如:
function foo(){//一个简单的模块,提供读取、修改message的功能
	var message = "some message";
	function getMessage(){
		return message;
	}
	function setMessage(newMessage){
		message = newMessage;
	}
	return {
		getMessage: getMessage,
		setMessage: setMessage
	}
}
var myMessage = foo();//先实例化模块,得到闭包的对象
console.log(myMessage.getMessage());//some message
myMessage.setMessage("other Message");
console.log(myMessage.getMessage());//other Message
  • 模块管理机制:
function myModel(){//以模块名来管理各个模块
	var space = {};//以模块名称为键,存储每个模块
	function addModel(name, dep, fn){//添加模块,参数为:函数名称、函数依赖、函数本体
		for(var i = 0; i < dep.length; i++){
			dep[i] = space[dep[i]];
		}
		space[name] = fn.apply(fn, dep);
	}
	function getModel(name){//调用模块
		return space[name];
	}
	return {
		addModel: addModel,
		getModel: getModel
	}
}
//使用
var firstModel = myModel();
firstModel.addModel("hello", [], function hello(){
	var fir = "hello ";
	return {
		fir: fir
	}
});
firstModel.addModel("world", ["hello"], function world(messg){
	var sec = "world!";
	function final(){
		console.log(messg["fir"] + sec);
	}
	return {
		final:final
	}
});
var check = firstModel.getModel("world");
check.final();

使用es6的模块机制:

//在hello.js文件里面
function hello(){
	return "hello";
}
export hello;//导出方法API

导入前面的模块:

//在world.js文件里导入hello.js文件
import hello from "hello";
function world(){
	return hello() + "world";
}
export world;

调用整个模块:

module world from "world";
console.log(world.world());
  • import 可以将一个模块的一个或多个API导入到当前作用域中,并分别绑定到一个变量上
  • module 会将整个模块的API导入并绑定到一个变量上
  • export 会将当前模块的一个标识符(变量、函数)导出为公共API
动态作用域
  • 词法作用域是在定义的时候确定的;动态作用域是在运行时确定的
function first(){
	console.log(a);//假如是词法作用域,输出的是2; 假如是动态作用域,输出的是3
				   //因为动态作用域不关心函数定义的时候,变量声明的值,而是关心在执行的时候,变量
				   //的值,而且在当前作用域没有找到声明的变量时,动态作用域也不会像词法作用域一样,
				   //不断的向外围作用域寻找,而是会查找调用栈,在这个例子中,会查找first()函数的
				   //调用栈,查找到second()的函数时,在其作用域内找到了对a变量的值,所以返回该值。
}
function second(){
	var a = 2;
	first();
}
var a = 3;
  • JavaScript并不具有动态作用域,但是 this 的使用规则和动态作用域的规则很像
this词法
  • 有时候 this 在函数调用的时候会失去与原函数的绑定,这里有三种方法可以保证 this 的绑定不会丢失:
var demo = {
	id : "awesome",
	cool : function coolDemo(){
		console.log(this.id);
	}
};
var id = "not awesome";
demo.cool();//awesome
setTimeout(demo.cool, 1000);//not awesome
  • 绑定 this :
    • 使用 var self = this:
    var demo = {
        id : "awesome",
        cool : function coolDemo(){
            var self = this;
            setTimeout(function coolTime(){
                console.log(this.id);//awesome
            }, 1000);
        }
    };
    var id = "not awesome";
    demo.cool();//awesome
    
    • 使用 bind() :
    var demo = {
        id : "awesome",
        cool : function coolDemo(){
            console.log(this.id);
        }
    };
    var id = "not awesome";
    setTimeout(demo.cool.bind(demo), 1000);//awesome
    
    • 使用 ES6 提供的箭头函数:
    var demo = {
       id : "awesome",
       cool : function () {
           return () => {//绑定的作用域是demo
               console.log(this.id);
           }
       }
    };
    var id = "not awesome";
    setTimeout(demo.cool(), 1000);//awesome
    
  • 对 this 的理解:
    • 使用 this 调用函数时不用显示的传入上下文对象,在写递归函数的时候尤为突出
    • this 是运行时绑定,绑定的值取决于它的调用栈,和函数声明时的位置无关,所以说 this 的作用域很像动态作用域,而不是词法作用域
    • ES6的箭头函数里面声明的 this 会绑定其所处的词法作用域,所以最好不要与原来的函数混用
  • this 绑定规则:
    • 默认绑定:没有使用修饰符调用函数,所以 this 绑定到默认的全局作用域
    function demo(){
       console.log(this.a);
    }
    var a = 2;
    demo();//2
    
    • 隐式绑定:调用位置有上下文作用域,即调用函数的时候有修饰符
    function demo(){
       console.log(this.a);
    }
    var obj = {
        a: 3,
        fn: demo
    };
    var a = 2;
    obj.fn();//3
    var fnc = obj.fn;
    fuc();//2,使用默认绑定规则
    
    • 显式绑定:使用 call() 、apply()、bind() 显式的将 this 绑定到特定作用域上
      • 特例:当绑定的作用域是 null 或者 undefined 时,显式绑定会失效,最后会使用默认绑定
    function demo(){
        console.log(this.a);
    }
    var obj = {
        a: 3
    };
    var a = 2;
    demo.call(obj);//3
    
    • new 绑定:
      • JavaScript没有严格意义上的构造函数,使用 new 操作符来调用一个函数一般会经过以下四步:
        1. 新建一个全新对象
        2. 对新对象执行 [[ 原型 ]] 连接
        3. 将函数调用的 this 绑定到新对象上
        4. 如果函数没有返回其他对象,则将返回新对象
     function demo(){
         this.a = 3;
     }
     var a = 2;
     var obj = new demo();
     console.log(obj.a);//3
    
    • 这四种绑定可以概括为:
      • 通过函数名调用函数,this 指向全局作用域;
      • 通过对象调用函数,this 指向该对象;
      • 通过call()、apply()、bind() 调用函数,this 指向参数作用域;
      • 通过 new 操作符调用函数,this 指向新建的对象。
  • 四种绑定方式的优先级:new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定
  • 使用箭头符号创建的函数,this会自动绑定创建时的上下文环境,并且不可改变
  • 有些情况下会在意料之外的使用默认绑定,导致污染全局作用域,可以使用一个空对象来保护全局作用域:
function demo(a, b){
   console.log(a + b);
}
var obj = Object.create(null);//和{}类似,但是不会创建Object.prototype这个委托,所以更空
demo.apply(obj, [2, 3])
对象
  • 基本类型:string、number、boolean、null、undefined、object、symbol

    • 除 object 外,其它类型为简单基本类型
    • 简单基本数据类型的 prototype 指向 null
    • 简单基本类型一般是一个字面量,并且是不能改变的,比如:
    var hello = "hello world!";//hello是基本类型string
    console.log(hello.length);//之所以能调用String对象的方法,是因为js自动把字面量转换为String对象
    console.log(hello.charAt(3));
    
  • 内置对象:String、Number、Boolean、Object、Function、Array、Data、RegExp、Error

  • typeof null 返回 object,但 null 是基本类型,之所以这样是因为不同的对象都会在底层以二进制的形式表示,js会判断二进制前三位为0的对象为 object,而 null 二进制全为0,所以会被判定为 object

  • 访问对象的值(引用值):

    • 属性访问:使用“.”来访问对象上的值,比如 a.b、a.c()
    • 键访问:使用“[“string”]”来访问对象的值,比如 a[“b”]
  • 数组:当使用类似于 a[“5”] = “something” 时,数组会当做 a[5] = “something” 处理,并且假如之前数组长度为2,执行完操作之后,数组的长度为6,中间的项用 empty × 3 来表示

    var t = [1, 2];
    t["5"] = 5;
    console.log(t);//(6) [1, 2, empty × 3, 5]
    
  • 对象复制:

    • 对于JSON安全(可以序列化为一个字符串,而且序列化之后可以反序列化为对象的对象),可以使用 JSON.parse(JSON.stringify(someobject)) 来获取到一个对象的复制
    • 使用ES6提供的 Object.assign(newObj, oldObj1, oldObj2) 来复制对象
  • 属性描述符:

    • writable(可写):是否可以修改属性值
    • enumerable(可枚举):是否可以通过枚举遍历来获得属性值,比如 for…in 操作
    • configurable(可配置):是否可以通过 defineProperty() 来修改属性值
    • 相关方法:
      • preventExtension():禁止扩展,禁止一个对象添加新的属性,并且保留已有属性
      • seal():密封,相当于调用 preventExtension() 后,再设置所有属性的 configurable: false,不仅不能添加新的属性,原有的属性也不能进行设置
      • freeze():冻结,相当于调用 seal() 后,再设置所有数据属性的 writable: false,禁止本身任何直接属性的修改
  • 判断对象属性是否存在于本身:

    • hasOwnProperty():会在自身属性里查找属性名是否存在
    • in:会遍历整个原型链查找属性是否存在
  • [[Get]] 和 [[Put]] 操作:

    • 当访问对象上的属性时,会触发 [[Get]] 操作,类似于一个执行函数
    • 当设置属性值时,会触发 [[Put]] 操作,同样是一个执行函数
  • getter 和 setter

    • 都可以修改默认的 [[Get]] 和 [[Put]] 操作
    • 可以单独只设置 getter 或者 setter,完整的设置类似于:
    var a = {
       	get v(){//获取属性值
            return this._v_;
        },
        set v(val){//设置属性值
            this._v_ = val;
        }
    };
    
  • 遍历:

    • 使用 for…in 遍历可枚举属性
    • 使用 forEach()、every()、some() 来迭代数组
  • js本身便没有类的机制,即使使用了 class 关键字来声明一个类
  • 其它语言的类的继承,实质上是对构造函数方法的复制(子类复制父类的方法),但 js 里面的子类和父类是通过原型链相互连接的,子类只是拥有父类函数方法的引用,当然父类也是只拥有函数方法的引用,因为方法不属于对象
原型
  • 几乎所有对象在创建的时候都会有一个非空的 [[prototype]] (原型链)属性,除了使用 Object.create(null) 创建的对象
  • 属性屏蔽:当在原型链下游对象对一个属性进行“=”操作时,比如 a.v = 3,且原型链上游对象存在属性 v,有以下几种情况
    • 属性 writable: true,这时会在下游对象使用 [[put]] 操作新建一个新的 v 属性,屏蔽上游的 v 属性
    • 属性 writable: false,则在非严格模式下默认操作失败,严格模式下会报 TypeError 错误
    • v 是一个 setter(只设置了 set 方法),则会静默操作失败
  • 创建一个所谓的构造函数时,其 .prototype 默认有一个共有的不可枚举的属性 constructor 属性,指向其本身,当使用 new 操作符来创建该构造函数的实例时,实例也会有一个 constructor 属性,会默认指向构造函数
  • 关联 Bar.prototype 和 Foo.prototype 的方法:
    • Bar.prototype = Object.create(Foo.prototype),需要抛弃原来的 prototype,所以会有一些轻微的性能损失
    • ES6:Object.setPrototype(Bar.prototype, Foo.prototype)
  • 字典:没有原型链的对象,可以通过 Object.create(null) 来创建,建立的对象没有 .prototype 和 .constructor 属性,所以非常适合用于存储数据
  • .prototype、._proto_ 和 .constructor 的区别
    • ._proto_ 表示的是 [[prototype]] 属性,指向创建它的对象,比如说下面代码:
    var a = {};
    console.log(a);//Object{._proto_: Object},因为所有自定义的对象原型都是内置对象Object
    
    • .prototype 和 .constructor 是创建函数时才会产生的两个属性,比如
    var a = function(){};
    //a没有存储函数实体,只存储了函数的引用,这个引用就是.prototype,而在函数实体所在的位置,有一个.constructor属性,指向a
    var v1 = new a();//v1会产生一个._proto_属性,指向a
    
行为委托
  • 模仿类的机制:
function Name(name){
    this.name = name;
}
Name.prototype.callName = function () {
    console.log(this.name);
};
function Message(name) {
    Name.call(this, name);
}
Message.prototype = Object.create(Name.prototype);//伪继承,实际上还是原型链引用
var v1 = new Message("Zhou");
v1.callName();//Zhou
  • 真正的委托模式:
var Name = {
    setName(name){
        this.name = name;
    }
};
var Message = Object.create(Name);//委托,实现方法共享
Message.callName = function(){
    console.log(this.name);
};
var zhou = Object.create(Message);//通过这种方式模拟创建实例
zhou.setName("Zhou");
zhou.callName("Zhou");
  • js 无法完美的实现类的机制,并且这种模仿行为会带来很多附带的问题,使代码复杂且难以理解,虽然 class 语法大大简化了类的许多操作,但也只是语法糖,其底层还是使用的原来的原型链机制。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值