你不知道的js【上卷】随看随记

本文深入探讨JavaScript中的词法作用域、函数作用域、块作用域及闭包概念,解析变量提升机制,并详细阐述this的绑定规则。此外,文章还介绍了对象属性的特性和操作方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第一部分

第二章 词法作用域

作用域逐级查找的规则,会产生“遮蔽效应”,也就是内部的标识符遮蔽了外部的标识符。
但是通过window.a可以访问那些被同名变量所遮蔽的全局变量。但非全局的变量如果被遮蔽了,无论如何都无法被访问到。


词法作用域的意义在于,无论函数在哪里被调用,无论如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。


词法作用域查找只会查找一级标识符。比如a,b,c这种。如果代码中引用了foo.bar.baz,词法作用域只会去查找foo标识符,找到后使用对象属性访问规则来接管对bar和baz的访问。


修改词法作用域:

  • eval
    eval中执行的代码可能会通过声明新变量等方式修改词法作用域。

但是在严格模式中,eval在运行时有自己的词法作用域,意味着其之中的声明无法修改所在的作用域。

  • with
    with本来是用于重复引用同一个对象中的多个属性的快捷方式:
var o = {a:1,b:2,c:3}

with(o){
    a =3;
    b=4;
    c=5;
}

with其实是根据传递进去的对象创建了一个全新的词法作用域。
因此当我们给一个并不存在的属性赋值时:

with(o){
    d:6;
}

o中并没有d标识符,因此会遵循作用域查找规则向上查找,而直到全局作用域都没有找到,就会自动创建一个全局变量(非严格模式)。因此最终d会被泄漏到全局中去,而没有达到给o添加属性的目的。

在严格模式中,with完全被禁止了。

eval和with在运行时修改或者创建新的作用域,非常影响性能
因为js引擎会在编译阶段进行一系列性能优化,其中一些优化依赖于根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。
但是遇到了eval和with,可能之前的优化都没有意义,因为无法知道传递给它们的会如何修改作用域,引擎就可能完全不做优化。

第三章 函数作用域和块作用域

区分函数表达式与函数声明:如果function是声明中的第一个词,那么就是一个函数声明,否则是一个函数表达式。


函数表达式可以是匿名的,而函数声明则不可以省略函数名。


let循环

for(let i=0;i<10;i++){
    console.log(i);
}
console.log(i);//ReferrenceError

let在循环中使用不仅将i绑定到所在的块级作用域中,并且它在每一次迭代中都会将上次迭代结束的值重新进行绑定,并重新赋值。

第四章 提升

编译阶段,包括变量和函数在内的所有声明都会被首先处理,也就是提升。而赋值或其他运行逻辑会留在原地等待执行阶段

注意,每个作用域内都会发生提升,而不只是全局作用域。函数作用域内也会进行提升。


函数表达式不会被提升,这是它与函数声明最大的区别。


函数首先被提升,然后是变量。
并且当变量声明重复时,后面的声明会被忽略:

foo();

var foo;

function foo(){
    console.log(1);
}
foo = function(){
    console.log(2);
}

会被引擎理解为如下形式:

function foo(){
    console.log(1);
}

foo();

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

由于函数声明会被提升到普通变量之前,因此var foo在foo函数声明之后是重复的声明,就被忽略了。

尽管重复的var声明会被忽略,但是后面的函数声明还是可以覆盖前面的函数声明:

foo();

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

最后会输出3.

第五章 作用域闭包

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


第二部分

第一章 this

引用函数自身的方式:

  • 针对具名函数,可以使用函数名来引用此函数:
function foo(){
    foo.count = 4;
}
  • 而对于匿名函数,由于没有名称标识符,只能使用不推荐的arguments.callee

this是在运行时绑定的,它的值取决于函数调用的方式。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当函数被调用时,会创建一个执行上下文,this是执行上下文中的一个属性。

第二章 this

判断this的值时,对象属性引用链中只有最后一层在调用位置中起作用:

function foo(){
    console.log(this.a);
}

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

var obj1 = {
    a:2,
    obj2:obj2
}
obj1.obj2.foo(); //42

赋值会造成this默认绑定的丢失:

var a = "global";
function foo(){
    console.log(this.a);
}

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

var bar = obj.foo;
bar(); //global

bar引用的是foo函数本身,和obj没有任何关系了。

传入回调函数时也是相同的:

var a = "global";
function foo(){
    console.log(this.a);
}

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

function doFun(fn){
    fn();
}

doFun(obj.foo);  //global

使用new调用函数,会执行下面的操作

  • 创建一个新对象
  • 将新对象的原型对象设为该函数的prototype值
  • 将新对象绑定到函数内部的this上
  • 如果函数没有返回其他对象,那么就返回这个新对象。

如果给call、apply、bind传入的是null或者undefined,那么这些值会在调用时被忽略,实际采用的是默认绑定规则,也就是this指向window或undefined。

比如说使用apply来展开一个数组:

function foo(a,b){
    return a+b;
}

foo.apply(null,[2,3])

当然在ES6中,可以直接使用…来展开数组:foo(...[2,3])


Object.create(null)可以创建一个空对象,比{}还空,因为并不会创建Object.prototype这个委托。
当我们不想指定call、apply、bind中的this时,就可以使用这样一个空对象,以免泄漏到全局中。

第三章 对象

一些内置对象:Array、Function、Date、RegExp、Error


对象中的属性名永远是字符串,如果你使用string以外的其他值作为属性名,那么它首先会被转换成一个字符串。数字类型的也不例外,要与数组中数字用法区分开。

var obj = {};

obj[true] = "foo";
obj[3] = "bar";
obj[obj] = "baz"

console.log(obj);
//{"3": "bar", "true": "foo", "[object Object]": "baz"}

可以看到所有属性名都被转换为了字符串,尤其是最后一个,将对象转换为了字符串。


数组也可以添加属性,但是数组的长度却并不会发生变化:

var myArr = [];
myArr.baz = "baz";
myArr.length; //0

但是如果添加的属性能够被转换为数字的话,就会变成一个数值下标,从而修改数组的内容而不是添加一个属性。

myArr["3"] = "baz";
myArr.length; //4

对象复制:

  • 对于可被JSON序列化的对象来说,可以使用JSON的方法进行深拷贝:
var copyObj = JSON.parse(JSON.stringify(obj));
  • Object.assign
    ES6新定义的方法,实现浅拷贝
Object.assign(target, ...sources)

第一个参数是目标对象,之后是一个或多个源对象。它会遍历一个或多个源对象的所有可枚举的自有键,并把它复制到目标对象上,最后返回目标对象。


属性描述符:

Object.getOwnPropertyDescriptor(obj, prop)

返回指定对象上一个自有属性的属性描述符。

var obj = {a:2};
Object.getOwnPropertyDescriptor(obj,"a");
//{
//  configurable:true,
//  enumerable:true,
//  value:2,
//  writable:true
//}

在创建普通属性时属性描述符会使用默认值,就像上面的a属性一样。我们也可以使用Object.defineProperty()来添加一个新属性或者修改一个已有属性(如果它是configurable的)并对特性进行设置:

var obj = {};
Object.defineProperty(obj,"a",{
    value:2,
    configurable:true,
    enumerable:true,
    writable:true
})

下面来介绍一下这些属性的特性:

  • Writable
    是否可以修改属性的值。
var obj = {};
Object.defineProperty(obj,"a",{
    value:2,
    configurable:true,
    enumerable:true,
    writable:false
})

obj.a = 3;
obj.a; //2

在严格模式下,尝试为writable为false 的属性赋值会报TypeError错。

  • Configurable
    属性是否可配置。若可配置,就可以使用defineProperty方法来修改描述符。
var obj = {};
Object.defineProperty(obj,"a",{
    value:2,
    configurable:false, //不可配置
    enumerable:true,
    writable:true
})

obj.a = 5;
obj.a; //5

Object.defineProperty(obj,"a",{
    value:2,
    configurable:true,
    enumerable:true,
    writable:true
})

最后一个defineProperty试图重新将此属性设置为可配置的,但是会产生一个TypeError错误。
不管是不是严格模式,尝试修改一个不可配置的属性描述符都会出错。 把configurable设为false是一个单向操作。

除了无法修改,configurable:false还会禁止删除这个属性。

Object.defineProperty(obj,"a",{
    value:2,
    configurable:false, //不可删除
    enumerable:true,
    writable:true
})
delete obj.a;
obj.a;  //2

可见删除失败了。

  • Enumerable
    此属性是否可枚举。
    可枚举的属性会出现在for-in循环中。

不变性

  • 常量属性:不可修改、重定义或者删除

    • configurable:false
    • writable:false
  • 禁止扩展:禁止一个对象添加新属性并保留已有属性。
    Object.preventExtensions(obj)

var obj = {a:2};
Object.preventExtensions(obj);
obj.b = 3;
obj.b;  //undefined

在严格模式下会抛出TypeError错误。

  • 密封
    Object.seal()会创建一个密封的对象,也就是不仅不能添加新属性,也不能重新配置或者删除现有属性。
    其实这个方法会调用Object.preventExtensions(obj)并将现有属性标记为configurable:false

  • 冻结
    Object.freeze()会创建一个冻结对象,其实是调用Object.seal()并将所有属性标记为writable:false。
    如此一来这个对象属性的值也都不能更改。


访问描述符:
可以使用getter和setter改写某个属性在读写时的默认操作。
getter是一个隐藏函数,会在获取属性值时调用;
setter是一个隐藏函数,会在设置属性值时调用。

当你给一个属性定义getter、setter时,这个属性会被定义为“访问描述符”。

对于访问描述符,js引擎会忽略它们的value和writable特性。

var obj = {
    get a(){
        return 2;
    }
}
Object.defineProperty(obj,"b",{
    get:function(){
        return this.a*2;
    },
    enumerable:true
})

obj.a; //2
obj.b; //4

上面两种方法都可以定义getter,二者都会在对象中创建一个不包含值的属性,对这个属性的访问会自动调用一个隐藏函数,它的返回值会被当作属性访问的返回值。

obj.a = 3;
obj.a; //2

由于我们只定义了a的getter,所以对a的值进行设置时会忽略赋值操作。而且即使有合法的setter,由于我们自定义的getter只会返回2,因此set操作是无意义的。

因此为了让属性更合理,需要定义setter。

var obj = {
    get a(){
        return this._a;
    },
    set a(val){
        this._a = val*2;
    }
}

obj.a = 2;
obj.a; //4

属性的存在性

  • in操作符 会检查属性是否在对象及其原型链

  • hasOwnProperty则只会检查属性是否在对象中,不会检查原型链。

Object.keys()和Object.getOwnPropertyNames()都只会查找对象自有的属性。

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

  • Object.getOwnPropertyNames()返回一个数组,包含所有属性,无论是否可枚举

有关枚举
一个属性设置了enumerable:false,那么:

  • 在for-in循环和Object.keys()中不会出现
  • 使用obj.propertyIsEnumerable(prop)可以检查对象上的属性是否可枚举。(不检查原型链)

遍历
使用for-in循环是无法直接获取属性值的,因为它遍历的是对象中所有可枚举属性,需要手动获取属性值。

第五章 原型

属性设置和屏蔽
当给一个对象设置属性时,obj.a = 2,过程比较复杂,:

  • 如果obj对象中包含a属性,那么就直接修改已有的属性值
  • obj对象中不包含a属性
    • 原型链中也找不到a属性,a属性被添加到obj上。
    • 原型链中有a属性
      • 原型链中的a属性是普通数据属性,直接在obj上添加一个a属性,发生了屏蔽
      • 原型链中的a属性被标记为只读,那么无法修改已有属性或者在obj上创建屏蔽属性。在严格模式下还会报错。总之这条语句会被忽略,不会发生屏蔽。
      • 原型链中的a是一个setter,那么就会调用这个setter,但是a不会被添加到obj上。

原型继承

Bar.prototype = Object.create(Foo.prototype)

Object.create(obj)的作用是创建一个新对象,并指定这个新对象的原型对象为obj。

以下两种是常见的错误做法:

  • Bar.prototype = Foo.prototype
    这只会让Bar.prototype直接引用Foo.prototype,当你为Bar.prototype添加属性时,实际上直接修改了Foo.prototype对象本身。如果你根本不需要Bar对象,那么直接使用Foo就好了。

  • Bar.prototype = new Foo()
    这是经典继承的做法,可以达到继承的目的,但是由于直接调用Foo函数,会有一些副作用,如为this(这里是Bar.prototype)添加实例属性,修改状态、注册到其他对象等,那么就会影响Bar的后代。

因此要创建一个合适的关联对象(继承),最好使用Object.create方法。

ES6提供了Object.setPrototypeOf()方法来修改对象的原型,也可以直接使用这种方法来实现继承:

Object.setPrototypeOf(Bar.prototype,Foo.prototype);

与Object.create方法实现继承相比,这种方式不会重写Bar.prototype对象,而是直接设定其原型,但是Object.create则会创建一个新的对象赋值给Bar.prototype,不过这种缺点比较无关紧要。


类关系

function Foo(){……}
Foo.prototype.baz = ……

var a = new Foo();

首先可以使用instanceof来判断:

a instanceof Foo;  //true

instanceof操作符左边是一个对象,右边是一个函数,主要判断a的原型链中是否有指向Foo.prototype的对象。

但是这种方法只能处理对象和函数之间的关系,不能判断两个对象之间有没有关联。

可以使用isPrototypeOf来判断。此例中是判断Foo.prototype与a的关系,因此可以这样使用:

Foo.prototype.isPrototypeOf(a);  //true

isPrototypeOf关心的就是在a的原型链上是否出现过Foo.prototype.

可以在任意两个对象之间使用isPrototypeOf():

var b = {};
var c = Object.create(b);

b.isPrototypeOf(c);  //true

我们可以直接获取一个对象的[[Prototype]]链:Object.getPrototypeOf(a) == Foo.prototype //true.
很多浏览器也支持一种非标准的方法来访问内部的[[Prototype]]属性:a.__proto__ === Foo.prototype; //true

.__proto__的实现大致上是这样的:

Object.defineProperty(Object.prototype,"__proto__",{
    get:function(){
        return Object.getPrototypeOf(this);
    },
    set:function(o){
        Object.setPrototypeOf(this,o);
        return o;
    }
})

Object.create的polyfill:

Object.create = function(obj){
    function F(){};
    F.prototype = obj;
    return new F();
}

Object.create还可以接收第二个参数,指定了需要添加到新对象中的属性名以及这些属性的描述符。

var obj = {a:2};
var anotherObj = Object.create(obj,{
    b:{
        enumerable:false;
        writable:true,
        configurable:false;
        value:3
    },
    c:{
        enumerable:true;
        writable:false,
        configurable:false;
        value:4
    },
});

anotherObj.hasOwnProperty(a);  //false;
anotherObj.hasOwnProperty(b);  //true;
anotherObj.hasOwnProperty(c);  //true;

anotherObj.a; //2
anotherObj.b; //3
anotherObj.c; //4
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值