月读书系列-你不知道的javascript(上)

在这里插入图片描述

一、作用域和闭包

1.1、作用域是什么?

1.1.1、编译原理

作用域是一套规则,用来存储和访问变量。任何编程语言都不开作用域,正是作用域这种存储和访问变量的能力将状态带给了程序,赋予了编程语言可以实现丰富功能的能力。

讲到作用域就不得不提两个重要角色:引擎编译器。引擎从头到尾负责整个javascript程序的编译和执行过程。编译器负责词法分析、语法分析、代码生成等脏活累活。

javascript 是一门编译型的语言,但它不是提前编译的,它的编译发生在在代码执行前的几微秒。

传统的编译语言的编译过程分为三个阶段:

  • 词法分析:这个过程主要将程序代码拆分成一个个的词法单元

    比如将var a = 2;拆分为var、a、=、2、;
    
  • 语法分析:这个过程将词法单元转换成一个由元素逐级嵌套所组成的语法结构树,这个树被称为“抽象语法树(AST)”

  • 代码生成:将抽象语法树(AST)转换成可执行代码的过程

javascript的编译过程:

javascript的编译过程比传统的编译语言要复杂的多,它会在语法分析和代码生成的阶段对代码进行性能优化,它的编译发生在代码执行前的几微秒(甚至更短)的时间内

1.1.2、理解作用域

这里提到作用域,我们要说有3个角色引擎编译器作用域
我们先以一个简单的语句为例来看下:

var a = 1;

首先是编译器工作,先是将这段代码分解成词法单元,然后将词法单元解析成一个树结构,然后在生成代码这一阶段时,编译器会先询问作用域是否有一个a的变量存在当前作用域中,如果有,则会忽略a变量的声明,否则会在该作用域中声明一个a变量,当一段程序被编译器编译完生成可执行代码,然后引擎执行它时,会对其中的变量进行查询,去询问当前作用域中是否有一个a变量,如果有,引擎就会使用这个变量,如果没有,引擎会继续查找该变量,如果引擎找到了就会把1赋值给a,如果没找到,引擎就会跑出一个异常

上述过程中引擎查询变量的方式主要分 LHS查询(赋值操作的目标)和RHS查询(赋值操作的源头)

LHS查询: 赋值操作左侧的查询,LHS查询试图找到变量的容器本身,从而对其赋值。
RHS查询: retrieve his source value(取到他的源值)

Demo1:找出一下代码的LHS查询和RHS查询

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

var c= foo(2);

首先当执行到foo(2)时,引擎会去问作用域是否有一个foo变量,这就是RHS查询,然后找到foo变量是个函数时,就会执行函数内部代码,首先这里有个隐式地赋值a=2;将2赋值给了a变量,这是LHS查询,然后执行var b =a时,这里要询问作用域a的值,是个RHS查询,然后将a的值赋值给b变量,这是LHS查询,执行到return a+b时,又要查询a和b的值,这两个查询都是RHS查询,最后将foo(2)的执行返回结果赋值给c变量,这是LHS查询,所以这里总共3处LHS查询、4处RHS查询

1.1.3、作用域嵌套

当一个块或者函数嵌套在另一个块或者函数中,就会发生作用域的嵌套,此时就会有多个作用域,因此就构成了一条作用域链,所以此时引擎执行时查询变量会依次从当前作用域开始查询(也就是作用域链的最底层),如果此作用域不存在,就会往上一层作用域查找,直至全局作用域为止,如果最后都没有,引擎就会跑出异常,你可以把整个作用域链比喻成一个建筑,从一楼开始查找,直到顶楼为止

1.1.4、异常

为什么我们前面要对LHS查询和RHS查询区分的这么清楚呢?因为这里涉及到不同的异常报错

  • LHS查询时,如果在顶层(全局作用域)下也没找到对应的变量,则会在全局作用域中创建一个新的变量,当然这得是在非严格模式下,如果在严格模式下也还是会报ReferenceError
  • RHS查询时,有两种异常,一种是如果在整个作用域链中都没找到所需的变量,则会抛出ReferenceError,还有一种是你找到了变量,但对变量进行了不合理的操作(假设他不是一个函数,但你却对他进行函数调用操作),引擎就会抛出TypeError

1.2、词法作用域(静态作用域)

什么是词法作用域

就是定义在词法阶段的作用域,换句话说正常情况下词法作用域是由你在写代码时将变量和块作用域写在哪里决定的

举个例子:

function foo(a){
	var b = a*2;
	function bar (c){
		console.log(a,b,c)
	}	
	bar(b*3)
}
foo(2);

上面代码就有3个逐级嵌套的作用域,bar函数创建的作用域foo函数创建的作用域全局作用域,这些都是在代码书写时就已经确认了的,在上面代码中,最后bar函数中打印a,b,c时,引擎就会先在bar创建的作用域中寻找这三个变量,发现a和b都没有,所以引擎就会往上层作用域中寻找,在foo创建的作用域中确实找到了,c变量则是在自己的bar作用域中就能找到,他就不会再往上层寻找

结论:作用域查找会在找到第一个匹配的标识符后停止,如果在多层嵌套的作用域中定义了同名的标识符,内层作用域的标识符会覆盖外层作用域的标识符,这叫做遮蔽效应

欺骗词法作用域

刚才前面说到的正常情况下词法作用域完全由写代码期间函数所声明的位置来定义的,但是js还是可以在运行时“修改”/“欺骗”词法作用域的…

  • eval:他是一个函数,可以接受一个字符串作为参数,并将这个字符串的内容视为插入到函数调用的位置中,就好像这个字符串在代码书写时就已经存在于函数调用位置

    举个栗子🌰:

    function foo(str,a){
    	eval(str). //欺骗
    	console.log(a,b)
    }
    var b =2;
    foo("var b = 3",1).  //1,3
    

代码解析:上述代码中eval调用中的“var b=3”这段代码会被当作本来就在那里的一样来处理,所以foo函数中打印a和b时,就会先在当前词法作用域中寻找对于变量,很显然,在foo函数创建的作用域中就能找到a和b,a=1,b=3,所以打印输出结果就是1,3,很显然eval函数修改了原有的词法作用域,原先正常在foo函数内没有b变量,引擎就会往上层作用域中寻找(全局作用域),会输出1,2

结论:在非严格模式下,eval函数会将接受到的字符串当作原本就属于eval函数调用的位置一样,来间接修改词法作用域

  • with:它是一个关键字,通常用在需要重复引用同一对象的多个属性时,就像你要修改一个obj对象的a、b、c属性

    	//假设原本 obj:{a:1,b:2,c:3}
    	
    	//原本修改方式:
    		obj.a=2;
    		obj.b=3;
    		obj.c=4;
    		
    	//使用with后:
    		with(obj){
    			a=3;
    			b=4;
    			c=5;
    		}
    

with关键字除了用在上述场景中,还可以用于“欺骗”词法作用域中,还是看个例子;

function foo (obj){
	with(obj){
		a=2;
	}
}
var o1={a:3}
var o2={b:3}
foo(01);
console.log(o1.a)  //2

foo(o2);
console.log(o2.a)   //undefined
console.log(a)  //2

代码解析:执行foo(01)时,with接收到了o1,该参数是一个对象引用,在with块内部,创建了一个新的词法作用域,然后执行了LHS查询,找到了并修改了o1对象中的a的值,所以打印o1.a时输出2,然后执行foo(o2)时,将对象o2穿入with块中,此时又在with块中创建了一个新的词法作用域,由于在新创建的作用域o2、foo函数创建的作用域以及全局作用域中都没有找到a属性,所以在全局作用域中创建了一个a变量(非严格模式下的LHS查询),所以打印输出o2.a为undefined,全局a为2

总结:with块中会自动凭空创建一个全新的词法作用域

js中不推荐使用eval和with,这里我们只是介绍下“欺骗”词法作用域的场景,使用eval和with会影响性能,因为js引擎会在编译阶段进行数项的性能优化,有些优化依赖于能够根据代码的词法进行静态分析,预先确定变量和函数的定义位置,方便执行时快速找到这些标识符,但如果引擎在代码中发现有eval和with,就无法对标识符位置进行优化,因为在词法分析阶段时无法知道eval()等会接收到什么代码,因此无法对其标识符进行位置确认,最终影响性能

1.3、函数作用域和块作用域

函数作用域的作用:

  • 隐藏内部实现,函数外部访问不了函数内部的变量或者函数,这也遵循一个”最小授权或者最小暴露“原则,这个原则就要求我们平常在开发过程中要最小限度的暴露必要内容,不然有时将内部内容过多的暴露后会造成一些不必要的异常和冲突
  • 规避冲突

函数表达式可以是匿名的也可以是具名的,但函数声明不可以省略函数名,只能是具名的

IIFE:立即执行函数表达式,他有两种书写方式:

方式一:

( function foo() { ..... } ) ()  //将函数foo用()包裹,将其变成了函数表达式,然后再用()调用函数

方式二:

(function foo(){ .... }() )    // 直接将第一种方式种的调用()移到了里面

IIFE还有一种进阶用法,就是把它们当做函数调用,并传递参数进去:

var a =2;
(function iife(global){
	var a =3;
	console.log(a);  //3
	console.log(global.a); //2
})(window)
console.log(a);  //2

将全局window对象穿入到iife函数中,iife函数用global形参接受window对象

块作用域:函数不是唯一的作用域单元,块作用域通常指在{。。。}内部的作用域,常见的块作用域的场景有with(前面提到的with会创建一个全新的作用域,这个作用域就是块作用域)、try\catch的catch分句{}中,在ES6以后引入了let和const,let关键字通常用在{}中,它为其声明的变量隐式的劫持在当前所在的块作用域中,白话就是let声明的变量只能在当前{}块作用域中访问,外部访问不了,const也是类似原理,但它声明的是一个常量,值不能被改变,之前我们学习的有var关键字,这个var关键字有一个特殊的性质就是会hulve块作用域,它定义的变量相当于在全局作用域中定义了一个变量,具体这三个关键字的区别可以访问我另外一篇专门讲述三者的文章 js中var、const、let之间的区别以及作用域

1.4、提升

本章主要讲述一个声明提升的问题,也就是说在某个作用域中的所有声明(变量和函数)都会被移动(提升)到作用域顶部,优先被编译器在编译阶段执行,剩余部分则还是在原位,在执行阶段执行

注意:只有声明会被提升,像函数表达式就不会被提升

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

原因是因为在当前作用域中(全局作用域),var a =2 会被拆解为var a +a=2两部分,var a 是变量a的声明,所以会被提升至作用域的顶部,所以上述代码正确的顺序如下:

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

既然刚才说了变量和函数声明都会被提升,就总会有个先后顺序,那到底是函数先提升还是变量呢?我们来看一个例子:

foo();    //1
var foo;    //变量声明
function foo(){    //函数声明
	console.log(1)
}
foo=function(){
	console.log(2)
}

从上面执行结果可以看出,是函数声明foo先被提升到顶部了,优先声明了一个函数foo,然后才是var foo的变量声明,很显然,这里的foo变量声明重复了会被忽略,然后再是执行foo(),所以最后执行结果是输出1

1.5、作用域闭包

闭包:函数可以记住并访问所在的词法作用域时,即使函数是在当前词法作用域外执行的,这时就产生了闭包。它是基于作用域这个概念的。

典型的一个列子:

function foo(){ 
    var a = 2; 
    function test(){ 
        console.log(a);    
    } 
    return test; 
} 
var func = foo(); 
func(); //2 

上面代码,函数test定义在foo函数内部,所以此时test函数的词法作用域链分别从内到外是test函数自身创建的作用域、foo函数创建的作用域、以及全局作用域,当执行func()时,其实就是全局作用域下执行了test(),这就相当于test函数在当前自身词法作用域外部执行,并且还访问到了foo函数作用域中的变量a(词法作用域链查找变量原理),所以我们说此时这里就产生了闭包

当某个函数可以记住并访问所在的词法作用域,那么就可以在其他地方使用这个闭包;并且这个被记住的词法作用域不会被销毁,可以一直被引用。

一个很常见的关于闭包的误解经常发生在循环中。比如:

for(var i=0; i < 6; i++) { 
    setTimeout(function(){ 
        console.log(i); 
    },0); 
} 

这里只会输出6个6。【Tip:这里定时器会等到循环执行完才会执行内面的内容-js执行机制相关内容】

这里我们使用了闭包+块代码,其中块代码的作用域是全局的,所以当执行完循环之后运行setTimeout中闭包之后,其中引用的i就是全局公共区域中的i,也就是6。所以最终输出6个6.

那么如何达到输出1到6的效果呢?我们可以通过作用域+闭包,解决循环中存在的问题,这里有两种方式:立即执行函数(IIFE)、块作用域

函数作用域:

for(var i=0; i < 6; i++) { 
    (function(j) { 
        setTimeout(function(){ 
            console.log(j); 
        },0) 
    })(i); 
} 

块作用域:

for(let i=0; i < 6; i++) { 
    setTimeout(function(){ 
        console.log(i); 
    },0) 
} 

谈到闭包,就不提一个很新的概念——模块模式。

模块模式其实就是借助了闭包的思想。要实现一个模块模式需要具备两个必要条件:

外部包裹函数+函数至少被调用一次返回实例;
至少返回一个内部函数,才能形成闭包。

编程语言中,比如 Java,是支持将方法声明为私有的,即它们只能被同一个类中的其它方法所调用。

而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。

下面的示例展现了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量。这个方式也称为 模块模式(module pattern):

var Counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }
})();

console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */

上述代码我们只创建了一个词法环境,为三个函数所共享:Counter.increment,Counter.decrement 和 Counter.value。

该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。

这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法作用域,它们都可以访问 privateCounter 变量和 changeBy 函数。

var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }
};

var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */

请注意两个计数器 Counter1 和 Counter2 是如何维护它们各自的独立性的。每个闭包都是引用自己词法作用域内的变量 privateCounter 。

每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量

二、this和对象原型

2.1、关于this

  • 为什么要使用this:this提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计的更加简洁并且易于复用
  • 关于this的两个误区:this既不指向函数本身,也不指向函数的词法作用域
  • this的真正指向:this实际是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用

2.2、this全面解析

调用栈(执行栈):也是也可以叫做执行栈,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文,js中每个函数在被调用的时候都会创建一个函数的执行上下文,当然这个调用栈最底下的是全局上下文,其次是哪个函数调用,就会把那个函数的执行上下文压入调用栈中,函数执行完后会弹出调用栈

this绑定规则:

  • 默认绑定:就是直接使用不带任何修饰的函数引用(说白了就是函数直接调用),这种情况下使用默认绑定,默认绑定,如果函数体在非严格模式下,则会默认绑定到全局对象上,严格模式下,会被绑定到undefined上

    备注:这里说的严格模式和非严格模式指的是函数体定义中是否处于严格模式,而不是函数调用的位置是否处于严格模式,这个不要搞错了

    举个例子:
    function foo(){
    	console.log(this.a)
    }
    var a =2;
    foo()   //2
    
  • 隐式绑定:简单来说,就是有某个对象(上下文对象)来调用函数,当然这个对象内部要有该方法的引用属性,例如:obj.foo()这种,这种情况叫做隐式绑定,隐式绑定会把函数调用中的this绑定到这个上下文对象,还需注意的是,如果对象属性引用链上有多个对象调用时,只有最后一个是生效的,看个例子:

    function foo(){
    	console.log(this.a)
    }
    var obj1={
    	a:1,
    	obj2:obj2
    }
    var obj2={
    	a:2,
    	foo:foo
    }
    obj1.obj2.foo() // 2
    

    但是这个隐式绑定通常会有一些特殊情况就是隐式丢失,换句话说就是看着明明是隐式绑定,但最后this会被绑定到默认绑定中(全局对象或者undefined)

    ①函数别名方式

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

    虽然bar是obj.foo的一个引用,但实际上他引用的是foo函数本身,所以bar()其实是一个不带任何修饰符的函数调用,所以应用了默认绑定方式

    还有一种情况:②传入回调函数

    function foo(){
    	console.log(this.a)
    }
    function doFoo(fn){
    	fn();   //fn其实就是foo函数本身,所以这里其实也是不带任何修饰符的调用,非严格模式下,所以指向window
    }
    var obj={
    	a:2,
    	foo:foo
    }
    var a ='global'  
    doFoo(obj.foo)     //"global"
    
    ---------------下面的setTimeOut函数回调也是同理------------
    function foo(){
    	console.log(this.a)
    }
    function doFoo(fn){
    	fn();  
    }
    var obj={
    	a:2,
    	foo:foo
    }
    var a ='global'  
    setTimeOut(obj.foo,100)   //"global" 
    //其实这里的setTimeOut可以拆解为function setTimeOut( fn,delay){ //等待delay毫秒 fn() },这样看就很清楚地看到和上面的情形是一样的了,也是不带任何修饰符的调用方式
    
  • 显示绑定:就是通过调用apply、call、bind方法,强行将this绑定到指定的对象上下文中(也就是方法接受的第一个参数)
    bind:会返回一个新函数,第一个参数就是你要指定的this上下文对象,他会把你指定的参数设置为this的上下文,不会立即执行函数,需手动调用
    call:接受的第一个参数也是你要指定的上下文对象,他会把你指定的参数设置为this的上下文,然后立即执行函数调用,它除第一个参数外,后面的参数就是函数接受的参数要一一列出
    apply:基本同call,只有接受的参数格式不同而已,它接受的参数是一个数组

  • new绑定:说白了就是,通过new 一个构造函数调用,会构造出一个新对象,并把这个新对象绑定到函数中的this上,如果,函数中最后没有返回其他对象,那么会自动将这个新创建的对象默认返回

    这里嗨哟声明一点:js中没有”构造函数“,这个”构造函数“也不是什么特殊的函数,就是也普通函数一样,只不过函数前如果通过new修饰符来调用了,我们此时称这个被调用函数为构造函数“,顺便再说一下,用new 调用函数时发生了什么?
    1、创建(或者说构造)了一个新对象;
    2、这个新对象会被执行[[ Prototype]]连接;
    3、这个新对象会绑定到函数调用的this;
    4、如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象

优先级:判断this可以按照下面的顺序:

1、函数是否在new中调用,如果是,this绑定的就是新创建的对象
2、函数是否通过call、apply、bind等显示绑定,如果是的话this绑定就是指定的那个对象
3、函数是否在某个上下文对象中调用(隐式绑定),如果是,this绑定的就是那个上下文对象
4、如果都不是的话,使用默认绑定,严格模式下绑定的是underfined,非严格模式下绑定的是window

当然也有一些特殊情况,this的绑定会出乎意料,我们来看下:

①将null或者undefined穿入call、apply、bing中作为this的绑定上下文对象,这种情况下走的是默认绑定

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

注意:如果使用null或者undefined可能会产生副作用,就是如果函数中有用到this,这样操作的话,会把this指向window,造成bug,所以如果要这样操作的话,建议传一个特殊的对象(DMZ),一个空的非委托对象,var φ =Object.create(null),这样就不会影响this的指向,使用起来更安全;

②间接引用

var a =2;
var o={ a:3,foo:foo }
var p={a:4}
o.foo()   //3
(p.foo=o.foo)()   //2

this词法:讲的是ES6中的箭头函数不会使用上述的几条this绑定规则,而是根据当前的词法作用域来决定this(或者说会继承箭头函数外层函数的this)

2.3、对象

对象的定义形式有两种:声明(文字)形式和构造形式

const a ={key:111}   //声明文字形式(常用这种)

const obj = new Object();  //构造形式
obj.key=111

内置对象:js中有一些对象子类型,通常被称为内置对象,像String、Number、Boolean、Object、Function、Array、Date、RegExp、Error这些其实都只是一些内置函数,可以通过new这些函数(爱这里当成构造函数),构造出一个对应类型的新对象

var hahaObj=new String(‘haha’) //hahaObj就是一个string对象
typeof hahaObj = "object"   //证明这个事实

思考:像var a = ‘哈哈’,这个a是一个字面量,并不是对象,为啥可以通过a.length等操作来操作呢?其实这时因为js引擎在必要的时候自动把字面量形式转换对象形式了,所以可以访问属性和方法,数字字面量也是同样的道理

对象的内容:是由一些存储在特定命名位置的值组成的,我们称之为属性

访问属性值的方式有两种“.”操作符或者 [ ] 操作符,但它们之间是有区别的,.操作语法通常被称为“属性访问”,[]语法通常被称为“键访问” ,.操作符要求属性名满足标识符的命名规范而[]操作符可以接受任意UTF-8\Unicode字符串作为属性名,像“h-a”这种属性名只能通过[]操作符才能访问,因为它并不是一个有效的标识符属性名;

可计算属性名:ES6增加了一种叫可计算属性名,它可以在文字形式中使用[]包裹一个表达式来当做属性名:

var prefix= "foo"
var obj={
[ prefix + " ha " ] : 111
}
obj [ "fooha" ]    //111 

说明:从技术的角度说,函数永远不会“属于”一个对象,一个对象中如果有属性访问返回的是一个函数,我们不能说这个函数是属于这个对象的

复制对象:这里说到复制就会涉及浅拷贝和深拷贝,这里不做具体详解了,有兴趣的可以访问我的这篇文章

属性描述符:ES5之前,js并没有提供可以直接检测属性特性的方法,比如判断属性是否可读,但从ES5开始所有属性都具备了属性描述符,一个普通的对象拥有4个属性描述符(也被称为数据描述符),我们常见的是value,但它实际上还有writable(可写)、enumerable(可枚举)、enumerable(可配置)

var obj = {
	a:2
}
Object.getOwnPropertyDescriptor( obj , 'a' )   

// 返回如下对象 {  
 value:2, 
 writable:true, 
 enumerable:true, 
 enumerable:true 
}

这里引出一个方法:Object.getOwnPropertyDescriptor( 对象 , ‘对象属性名’ ) ,它可以获取到某个对象某个属性的属性描述符

当然你可以添加或者修改已有属性:

var obj ={} ;
Object.defineProperty(obj,"a",{   //给一个空对象添加了一个a属性
	value:222,
	writable:true,
	enumerable:true,
	configurable:true,
})
obj.a   //2

接下来我们分别来介绍下这几种属性描述符!!!!

  • writable:决定了是否可以修改这个属性的值(value),它如果设置为了false,那么你再通过对象.该属性=另一个值,这种方式去修改属性值就不生效了,属性值还是原先的值
  • configurable:决定属性是否可配置,意思就是能否通过Object.defineProperty()来修改属性的配合,如果是false,则不能再通过这个方法来修改或者添加属性了,同时也不能通过delete 来删除对应属性了,不生效
  • enumerable:决定该属性是否能出现在对象的属性枚举中(for …in循环),如果是false就不会出现在枚举中

不变形:有时候你会希望属性或者对象是不可改变的,所以有如下几种方式可以实现:

1、结合writable:false + configurable:false可以创建一个真正的常量属性(不可修改、重定义、删除)
2、使用Object.preventExtensions(对象)来禁止一个对象添加新属性并且保留已有属性(阻止对象扩展)
3、使用Object.seal()会创建一个“密封”的对象,密封后不能添加新属性,也不能重新配置或者删除,但是可以修改属性的值
4、使用Object.freeze()会创建一个冻结对象

[ [ GET ] ]: 获取属性的值,实际上就是内置的[[GET]]操作,首先会在对象中找,如果找不到,会在原型链中找,详细见下面2.5原型章节

[ [ PUT ] ]: 详细见下面2.5原型章节

访问描述符:Getter和Setter

存在性:判断对象中是否存在某个属性,可以有两种方式,第一种是in操作符,它会检查属性是否在对象及原型链中,还有一种方式是**hasOwnProperty()**只会检查属性是否在对象中

注意:其实in操作符检查的是属性名,而不是属性值,例如:4 in [1,2,4],为false,因为数组中并没有4这个属性名,有的只有0,1,2

2.4、混合对象 “类”

这块没什么核心内容,主要阐述了类是一种涉及模式,js中并没有类的语法,js中有混入模式,类似于模拟类的复制行为,但是通常会产生丑陋和脆弱的语法,得不偿失,所以不建议使用这种混入模式,所以这里也就不多介绍了

2.5、原型

[[Prototype]]:

js中的对象都会有一个特殊的[[ prototype ]]内置属性,它有什么用呢?前面一章介绍了当你在查询一个对象中的属性时,实际会触发对象内部的[[ Get ]] 操作,整个查询过程就是先在自身对象中找是否有这个属性,如果自身对象中没有,则会沿着对象的原型链继续往上查找,这里说的原型链指的就是这个对象本身通过[[ prototype ]]属性指向另一个原型对象,然后原型对象也有自己的原型对象,这一系列对象的链接被称为“原型链”。

举个例子:

const obj ={
	a:1
}
const obj2 = Object.create( obj ); 
console.log(obj2.a)   //1

代码解析:这里用到了一个方法Object.create( 源对象),该方法会创建一个新对象,这个新对象内部的[[prototype]] 属性就会指向传入的源对象,所以显然最后打印obj2.a时,自身没有这个属性,然后就沿着原型链去找,在obj对象中找到了就输出了

补充:这里要提一个for…in遍历,它在遍历对象时的原理和刚才这个查找原型链相似,只要能通过原型链访问到的属性都会被枚举,它也是查找整条原型链

var anotherObject = { 
	a:2
};
// 创建一个关联到 anotherObject 的对象
var myObject = Object.create( anotherObject );

for (var k in myObject) { console.log("found: " + k);
}
// found: a
("a" in myObject); // true

Object.prototype

接下来我们来思考一个问题,那这个原型链的尽头是哪呢?

答案是:Object.prototype,因为,js中任何对象都是源于Object,所以任意对象才会拥有一些对象的通用功能(tostring()或者valueof())

属性设置和屏蔽:

刚才在前一章有个遗留问题没细讲,好了我们在这里结合原型来讲下具体js中是如何设置对象属性的,给一个对象设置属性并不仅仅是添加一个新属性或者修改已有的属性值。

以myObject.foo = “bar”;为例

1、首先判断这个对象本身是否已存在属性名为foo,如果已存在,则会直接修改已有的属性值;

2、如果自身没有该属性,则会遍历原型链,如果原型链上也找不到 foo,foo 就会被直接添加到 myObject 上(也就是在对象中新创建一个foo属性)。

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

"类"(原型继承)

js中其实并不存在类,它只有对象

但是多年以来,JavaScript 中有一种奇怪的行为一直在被无耻地滥用,那就是模仿类
那具体js是怎么实现,或者说怎么来模仿类的呢?

这里要利用到函数的一个特性就是:所有的函数默认都会拥有一个 名为 prototype 的公有并且不可枚举的属性,它会指向另一个对象

function Foo() {  //普通函数
}
Foo.prototype; // Foo函数的原型对象

这个对象通常被称为 Foo 的原型,因为我们通过名为 Foo.prototype 的属性引用来访问它。这个原型对象在函数创建的时候就存在了,然后通过new Foo()创建出来的实例对象的[[ prototype ]]属性会关联到Foo.prototype这个原型对象上,我们来看下面这个例子:

function Foo() { //Foo函数
}
var a = new Foo();   //创建一个a实例
a.__proto__ ===Foo.prototype  //__proto__属性等同于对象内部的[[prototype]]属性

总结:在 JavaScript 中,并没有类似的其他面向类语言的复制机制(像用模具制作东西一样),它内部只能通过创建多个对象,然后让这些对象互相关联(它们的[[prototype]]关联的是同一个对象),就像上面的例子定义函数Foo的时候就默认创建了一个Foo.prototype对象,然后再new时又创建了一个a实例对象,new的过程中间接的将a对象通过[[prototype]]属性关联到了Foo.prototype对象,所以a对象中就能访问到Foo.prototype对象中的属性和方法了,所以这就类似于类的继承,但是在js中我们称------------“原型继承”,这里可能称继承一词会引起误解,继承就意味着复制操作,但js中并不会复制对象属性,而是通过委托访问另一个对象的属性和函数,委托这个术语可以更加准确地描述 JavaScript 中对象的关联机制。

“构造函数”

构造函数理解:在 JavaScript 中对于“构造函数”最准确的解释是,所有带 new 的函数调用。 函数不是构造函数,但是当且仅当使用 new 时,函数调用会变成“构造函数调用”

函数在定义的时候,除了会生成一个原型对象外,这个原型对象内置有一个公有并且不可枚举的属性 .constructor这个属性引用的是对象关联的函数(本例中是 Foo)

function Foo() { 
}
Foo.prototype.constructor === Foo; // true
var a = new Foo(); 
a.constructor === Foo; // true 

代码解析:上述的代码证实了上述理论,constructor属性指向关联的函数,但为什么通过Foo函数创建的a实例内部怎么也有constructor属性呢?
不知你还记不记得前面讲过的查找对象属性的过程,自己本身没有就往原型链上找,这里其实a对象本身并没有constructor属性,而是在原型链上的Foo.prototype对象上找到的,间接引用了;

原型对象被修改:当然还有一种情况会出现,就是修改了原型对象,这样的话就会打破原先的对应关系,废话不多说,来看个例子

function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新原型对象newObj
var a1 = new Foo();
a1.constructor === Foo; // false
a1.constructor === Object; // true

代码解析: constructor 属性只是 函数在声明时创建的原型对象的默认属性,不要认为是谁构造了对象,这个属性就指向谁,上面a1对象是有Foo函数构造出来的一个实例对象,但是由于第二行改变了Foo函数的原型对象,指向了新的newObj对象(这里方便说明临时叫的),所以此时a1.constructor(更准确点说是a1指向的原型对象的constructor)就是newObj的constructor,但是这个对象本身也没有这个属性,所以沿着它的原型链一直找就找到了Object.prototype了,然而这个对象的constructor属性指向内置的 Object(…) 函数。

当然修改了原型对象后还是可以给新原型对象添加一个constructor属性来关联上原先的函数

function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新原型对象
// 需要在 Foo.prototype 上“修复”丢失的 .constructor 属性 // 新对象属性起到 Foo.prototype 的作用
// 关于 defineProperty(..),参见第 3 章 Object.defineProperty( Foo.prototype, "constructor" , {
enumerable: false,
writable: true,
configurable: true,
value: Foo // 让 .constructor 指向 Foo
} );

注意:.constructor 是一个非常不可靠并且不安全的引用。通常来说要尽量避免使用这些引用。

那既然上面说的重新修复constructor属性这种方式不推荐使用,那有没有一个标准并且可靠的方法来修改对象的 [[Prototype]] 关联呢?答案是有的,总共两种方式,具体看下面

  1. ES6 之前需要抛弃默认的 Bar.prototype Bar.ptototype = Object.create( Foo.prototype );
  2. ES6 开始可以直接修改现有的 Bar.prototype Object.setPrototypeOf( Bar.prototype, Foo.prototype );

如果忽略掉 Object.create(…) 方法带来的轻微性能损失(抛弃的对象需要进行垃圾回 收),它实际上比 ES6 及其之后的方法更短而且可读性更高。

内省(反射)

检查一个实例(JavaScript 中的对象)的继承祖先(JavaScript 中的委托关联)通常被称为 内省(或者反射)。

通常有什么方法可以找出对象 的“祖先”(委托关联)呢?
1、instanceof()方法

function Foo() { 
}
var a = new Foo();
a instanceof Foo; // true

代码解析:instanceof 操作符的左操作数是一个普通的对象,右操作数是一个函数。instanceof 回答的问题是:在 a 的整条 [[Prototype]] 链中是否有指向 Foo.prototype 的对象? 这个方法只能处理对象(a)和函数(带 .prototype 引用的 Foo)之间的关系

如果你想判断两个对象(比如 a 和 b)之间是否通过 [[Prototype]] 链关联呢?

2、Foo.prototype.isPrototypeOf(a)
这个方法指的是Foo.prototype 是否在a的整条 [[Prototype]] 链中?

2.6、行为委托

本章主要讲述了一种强大的设计模式(行为委托),行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类的关系。JavaScript 的[[Prototype]] 机制本质上就是行为委托机制

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ronychen’s blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值