【javascript】《你不知道的javascript》上卷笔记

书看了一段时间就忘记了,这次从新开始看,以博客的方式记录笔记,可能比较杂乱,如果你看了有用也是极好的,更建议你自己去买书或者看电子书


Let’s beginning


编译原理

通常将js归类为动态解释性语言,实际上javascript 引擎进行编译的步骤 和 传统编译语言非常类似。

1,分词/词法分析(Tokenizing/Lexint)
      比如我们声明了一个 var a = 2,这时候这段代码会被分解为, var,a, =, 2 这些词法单元,空格是否会被当作词法单元,取决于空格在这门语言中是否有意义,比如在js中,空格不会被解析为词法单元。

2,解析/语法分析
     这个过程是将第一部转换成的词法单元流(数组)转换成一个由元素逐渐嵌套所组成的代表了程序语法的结构的树,这和react里面那个ast抽象语法树实际上是类似的概念。react是将组件理解为一个抽象的语法树。“抽象语法树“ 全名简称”abstract syntax tree“。
     我们以上面的为例,var a = 2 , 在整个抽象语法树里面可能会存在一个VariableDeclaration的顶级节点,抽象语法树为一个顶级对象,里面的一个一级节点就为VariableDeclaration对象,这个对象有一个Identifier它的值为a,它也有一个叫做AssignmentExpression的子节点。 AssignmentExpression节点有一个叫做NumericLiteral的子节点(它的值为2)
     写成对象可能是这样:

  GlobalAST: {
       VariableDeclaration: {
             Identifier: 'a',
             AssignmentExpression: {
                  NumericLiteral: 2
             }
       }
  }

3, 代码生成
     将AST转换成为可执行代码的过程称之为代码生成。这个过程与语言,目标平台等信息息息相关。
     比如我们将一份js代码,放在浏览器里面 和 nodeJS环境下,执行出来的结果可能是不同的。


作用域

      引擎:比如google v8 引擎。负责整个js程序的编译以及执行过程。

      编译器: 编译器是引擎的好朋友,编译器负责语法分析->转换抽象语法树->代码生成,然后引擎才能执行这些代码。

      作用域: 作用域是引擎的另外一个好朋友,负责收集并维护由所有声明的标识符(变量)组成一些列查询。


LHS RHS

      还有一个重要的概念,LHS RHS 左查询 和 右查询

      LHS 去找到赋值的目标也【就是变量】,有一次赋值操作,就又一次LHS查询

      RHS 去找到变量的值,就有一次RHS查询

小测验:

    function foo(a) {
          var  b = a;
          return a + b;
     }

   var c = foo(2)

   //有多少次LHS 查询? 3  what is the detail ?
   //有多少次RHS 查询? 4  what is the detail ?

如果在LHS查询 和 RHS查询中遇到了错误怎么办呢?

RHS
      console.log(a) 这是执行的RHS,我们可以在chrome的控制台里面执行一次。
会报错: VM264:1 Uncaught ReferenceError: a is not defined
      RHS查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出ReferenceError的错误。
      如果RHS查询到了一个变量,但是对这个变量执行不合理的操作,比如对一个number类型的变量执行split操作,就会报错,TypeError【表示作用域判别变量成功了,但是对结果的操作是非法的,就会报这个错误】。

LHS
      如果执行LHS查询的时候,如果在顶层(全局作用域)中无法找到目标变量,全局作用域中会
      创建一个具有该名称的变量,并将其返回给引擎。【前提非严格模式下,而且es6的 let const等不会存在变量提升。】


定义函数的两种方式

      以function开头的是函数声明的方式,其他的是函数表达式,其实setTimeout(function(){}),也是函数表达式,只是他是一个匿名函数表达式。

      函数表达式是可以匿名的,但是匿名的函数声明在javascript是非法的,比如你不可以这样写 function(){ console.log(‘匿名函数声明’) },但是你可以这样写: var fname = function() { console.log(‘匿名函数表达式’) }

      更加建议使用具名函数表达式。比如上面的setTimeout,我们可以这样写:

 setTimeout(function timeoutHandler() {
	console.log('I waited 1 second')
 }, 1000)

1-代码可读性更高
2-出现异常的话,在栈中追踪不会出现有意义的函数名,只会出现anoynimous function,调试起来会困难不少。

==作用域 ==

      ES6以前没有块级作用域的概念,只有函数作用域和全局作用域。
      ES6出现了 let const 锁定一个块级作用域


提升

var a = 2 这句代码实际上有两个步骤来完成,编译阶段,和执行阶段。
编译阶段 转换成ast,然后转换成可执行代码 根据前面的知识点这是编译器来完成。 第二阶段 执行可执行代码 根据前面的只是点这是由引擎来完成。 第一阶段实际上就包含来变量的声明阶段,第二阶段才是执行的赋值阶段。

所以 var a = 2 有两个步骤
第一步: var a 声明
第二步: a = 2 赋值

提升是什么意思,可以理解为先声明,然后在执行阶段做赋值等其他操作。提升仅仅是指在当前的作用域提升【除去特殊情况】, 下面这个例子,可以看到 b仅仅提升了声明在函数内的作用域,对外面的全局作用域并没有影响。

a = 2
var a
console.log(a)
function () {
	console.log(b)
	var b = 3
}
console.log(b)

每个作用域都会提升,只会在各自的作用域提升。
tip:
      1, 函数声明和变量都会提升。
      2, 函数表达式不会被提升。
      3, 函数声明首先会被提升,然后才是变量。

闭包

书面语:函数可以记住并且访问现在的词法作用域时,就产生了闭包,即使函数是在当前词法呕用语之外执行的。

函数 内引用了一个外部作用域的变量,函数执行的时候能够获取到外层作用域的该变量值。

闭包的应用—模块化

闭包来实现私有化变量,实现模块化。
可以先看之前的一个例子,闭包私有化变量

	function initGoodInfo({name: '', nums: 0, unit: 0}) {
		let _name = name,
			_nums = nums,
			total = nums * unit
		const getTotalPrice = () => {
			return total
		}
		const getDetailInfo = () => {
			return ''
		}
		return {
			getTotalPrice: getTotalPrice,
			getDetailInfo: getDetailInfo
		}
	}

模块有两个关键的特性:
      1, 一个被调用的外部包装函数,用来创建外围作用域。
      2,这个包装函数的返回值必须包含至少一个内部函数的引用【这个引用至少引用外部包装函数的局部变量】,这个函数才拥有包装函数内部作用域的的闭包。


this对象

     之前做过一次对于this的记录:点击这个链接

      this不是编写的时候决定,是运行的时候决定。this绑定和函数声明的方式无关,和函数的调用方式有关。

      this实际上是函数在调用时建立的一个绑定,它指向什么完全由函数被调用的调用点来决定。

看下面的例子,你可以在chrome console里面执行这段代码:

var a = 'global string a'
function foo() {
	console.log(this.a)
}
const obj = {
	a: 'local string a',
	foo: foo
}
// 谁调用的这个函数,谁是this
obj.foo() // 'local string a'  


// 谁调用的这个函数,谁是this,这里没有xx.fooRef() 默认为全局作用域
// 传递的只是一个引用
const fooRef = obj.foo
fooRef() // 'global string a'

this绑定的方式:
隐式绑定:
     默认的方式
显示绑定:
     call/apply: 一样的效果,参数的传递方式不同。
     new :构造函数使用【特殊点,return一个对象,则返回这个对象,如果返回一个非对象,无效,默认返回一个构造的对象】
     bind: 返回一个函数,这个函h数的this会永久绑定到bind的第一个参数
     总结:
1, 函数是和new 一起被调用的吗(new 绑定)?如果是,this就是新构建的对象。

	var bar = new foo()

2, 函数如果是用call 或者apply被调用(明确绑定),甚至是硬绑定在bind的第一个参数中,如果是,那么this就指代明确的对象[call/apply/bind的第一个参数]。

 const bar = foo.bind(obj)

3,函数是用环境对象(也称拥有者或者容器对象)被调用的吗(隐含绑定)?如果是,this就是致那个拥有者。

	const obj = {
		a: 'something',
		foo: function() {
			console.log(this.a);
		}
	}
	obj.foo()

4, 使用默认的this(默认绑定),严格模式下是undefined,非严格模式就是全局对象,在chrome下是window,在node环境下是global。

 // 全局模式下 chrome的console里面,执行 foo()
 // is equal to 
 window.foo()

对象属性命名和复制

如果以string意外的其他值作为属性名,那么它首先会被转换为一个字符串。即使是数字也不会例外。虽然在数组下标中使用的是数字,但是在对象属性名中数字会被转换为字符串。数组和对象中数字的用法是不同的。

	var obj = {}
	obj[true] = "foo"
	obj[3] = "bar"
	obj[obj] = "fighter"
	
	// then
	obj["true"]
	obj["3"]
	obj["object Object"]

复制对象的一个方法[这个方法由于时区问题转换date的时候会出现错误]
请自行搜索,JSON.stringify() date 关键字

 var newObj = JSON.parse(JSON.stringify(someObj))

对象属性的一些控制

writable : 是否可以修改属性的值。

configurable: 可配置性,如果设置为false,第二次给该属性调用defineProperties会报错。如果是false,阻止修改属性,阻止删除属性。

enumerable: 是否可枚举【可以理解为是否可以被遍历出来】

	const obj = {}
	Object.defineProperty(obj, 'a', {
		writable: true,
		configurable: true,
		enumerable: true
	})

阻止对象添加新的属性,但是不能阻止删掉该对象属性:

	const obj = { name: 'kevin' }
	Object.preventExtensions(obj) 
	delete obj.name
	console.log(obj) // { }

定义一个对象常量属性 ,这个属性不能被修改,删除:

	const obj = {}
	Object.defineProperty(obj, "CONST_NUMBER", {
		value: 7,
		configurable: false,
		writable: false
	})

Object.seal()方法 是同时调用了Object.preventExtensions 以及将configurable 设置为false。
Object.freeze()方法是调用了Object.seal 以及将属性writable设置为false

检查属性是否属于对象

in
'name' in obj 会检查原型链上面是否存在该属性

hasOwnProperty
obj.hasOwnProperty('name') 只会检查属性是否在obj对象中,而不会检查[[prototype]]链 。但是这个可能会出错,hasOwnProperty是来自于Object.prototype,可能有的对象没有链接到这个对象。 比如Object.create(null),这个对象的prototype是null,并没有和Object.prototype链接起来

遍历对象

for in 会找原型链,获取的是对象属性或者数组下标, 而不是属性值,可作用于数组或者对象
for of ES6新增的,遍历数组用的,可作用于数组 Map Set,默认对象是不行的,会报错。因为他们没有实现迭代器方法。数组 Map Set 的’Symbol.iterator’方法。

	const obj = {
		name: 'kev',
		age: 18
	}
	for (let value of obj) {
		console.log(vallue)
	}
	// 会报错  Uncaught TypeError: obj is not iterable
	
   // 我们可以手动给该对象实现迭代, 比较重要
   Object.defineProperty(obj, Symbol.itertor, {
		enumerable: false,
		writable: false,
		confurable: true,
		value: function() {
			var o = this
			var idx = 0
			var keys = Object.keys(o)
			return {
				next: function() {
					return {
						value: o[keys[idx++]],
						done: (idx > keys.length)
					}
				}
			}

		}
	})

instanceof

a instanceof Foo在a对象的整条[[Prototype]]链中是否又指向Foo.prototype 的对象

委托

funcgion Foo(){}
const foo = new Foo()
foo.constructor === Foo

foo 并没有constructor 属性, 它会委托[[Prototype]]链 (原型链)上的Foo.prototype。

内部委托

var anotherObject = {
	cool: function() { console.log('cool') }
}
var myObjct = Object.create(anotherObject)

myObject.doCool = function(){
	this.cool() // 内部委托
}
myObject.doCool()// cool

//我们这里调用的是doCool方法是实际存在于myObject对象的
//比我们直接调用myObject.cool()更加清晰。

内部委托比起直接委托可以让API接口设计更加清晰。

posted on 2018-11-20 14:17  狂奔的冬瓜  阅读(231)  评论(0编辑  收藏  举报