zhangzheng-zz / blog

1 stars 0 forks source link

你不知道的 Javascript(上卷) #7

Open zhangzheng-zz opened 3 years ago

zhangzheng-zz commented 3 years ago

第一部分 作用域和闭包

第1章 作用域是什么

zhangzheng-zz commented 3 years ago

第2章 词法作用域

词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。编译的词法分析阶段基本能够知道全部标识符(变量、函数)在哪里以及是如何声明的,从而能够预测在执行过程中如何对它们进行查找欺骗词法作用域不推荐使用JavaScript 中有两个机制可以“欺骗”词法作用域:eval(..)with。运行时修改了词法作用域。(例如属性遮蔽) 这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们。

zhangzheng-zz commented 3 years ago

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

函数作用域: 函数是JavaScript中最常见的作用域单元。本质上,声明在一个函数内部的变量函数会在所处的作用域中“隐藏”起来,这是有意为之的良好软件的设计原则。 e.g

function foo() { 
    var a = 2; 
    function bar() {} 
}
window.a
// undefined
window.bar()
// Uncaught TypeError: window.bar is not a function

块级作用域: 函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指{ .. } 内部)。从ES3 开始,try/catch 结构在catch 分句中具有块作用域。 e.g

try {} catch (error) {
  // 块作用域
  function f() {
    console.log("f")
  }
  var a = 1
}
window.a
// undefined
window.f()
// Uncaught TypeError: window.f is not a function

ES6中变量块级作用域的产生: 一般来说,在 {...}中通过 var声明的变量属于全局属性,挂在全局作用域上:

if(true) {
  // 不会有块级作用域
  function f() {
    console.log("f")
  }
  var a = 10
}
window.a
// 10
window.f()
// f

ES6中,可以通过letconst来产生变量的块级作用域

if(true) {
  // 有块级作用域
  let k = 10
}
undefined
window.k
//undefined

通过立即自执行函数可以隐蔽变量以及函数

(function() {
  var a = 1
  function bar() {
    console.log("bar")
  }
})()
zhangzheng-zz commented 3 years ago

第4章 提升

第1章讲过,var a = 1JavaScript引擎看作两个过程:

函数与变量提升: 所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升, 并且,函数提升优先于变量提升。 e.g

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

会输出1 而不是2 !这个代码片段会被引擎理解为如下形式:

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

注意,var foo 尽管出现在function foo()... 的声明之前,但它是重复的声明(因此被忽略了),因为函数声明会被提升到普通变量之前。但出现在后面的函数声明还是可以覆盖前面的(函数声明)。

一个普通块内部的函数声明通常会被提升到所在作用域的顶部: e.g

foo(); // "b"
var a = true;
if (a) {
function foo() { console.log("a"); }
}
else {
function foo() { console.log("b"); }
}

注意:函数表达式不会提升 e.g

// 函数不提升
f()
var f = function() {
  console.log("f")
}

// 函数提升
f()
function f(){
  console.log("f")
}
//TypeError: f is not a function
zhangzheng-zz commented 3 years ago

第5章 作用域闭包

闭包:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。 循环与闭包: 下面来看一个经典的问题:

for (var i=1; i<=5; i++) {
    setTimeout( function timer() {
        console.log( i );
    }, i*1000 );
}

运行时会以每秒一次的频率输出五次6 使用 IIFE 进行改造,创造出作用域:

for (var i = 1; i <= 5; i++) {
  (function () {
    setTimeout(function timer() {
      console.log(i);
    }, i * 1000);
  })();
}

运行结果还是一样,五次6 如果作用域是空的,那么仅仅将它们进行封闭是不够的。 每次迭代,使用使用变量j来存储,形成独立的词法作用域

for (var i = 1; i <= 5; i++) {
  (function () {
    var j = i;
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })();
}

或者

for (var i = 1; i <= 5; i++) {
  (function (j) {
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })(i);
}

运行结果为每秒增加1输出 使用变量的块作用域解决:

for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, i * 1000);
}

模块模式

MyModules.define("bar", [], function () { function hello(who) { return "Let me introduce: " + who; } return { hello: hello, }; });

MyModules.define("foo", ["bar"], function (bar) { var hungry = "hippo"; function awesome() { console.log(bar.hello(hungry).toUpperCase()); } return { awesome: awesome, }; });

var bar = MyModules.get("bar"); var foo = MyModules.get("foo");

console.log(bar.hello("hippo")); // Let me introduce: hippo foo.awesome(); // LET ME INTRODUCE: HIPPO

zhangzheng-zz commented 3 years ago

第二部分 this和对象原型

第1章 关于 this

this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用

zhangzheng-zz commented 3 years ago

第2章 this 全面解析

绑定规则: 找到调用位置,然后判断需要应用下面四条规则中的哪一条

一、默认绑定:无法应用其他规则时的默认规则

三、显式绑定call、apply、bind

自定义 bind 函数:

function foo (something) {
  console.log(this.a, something)
  return this.a + something
}

var obj = {
    a:2
}

// 自定义 bind 绑定函数
function bind (fn, obj) {
  return function () {
    return fn.apply(obj, arguments)
  }
}

var bar = bind( foo, obj );
bar(3) // 5

ES5 中提供了内置的方法Function.prototype.bind,可以直接使用:foo.bind(obj,2)()

四、new绑定 new 执行了如下步骤:

规则优先级: new 绑定 > 显式绑定 (call、apply、bind)> 隐式绑定(绑定上下文obj等调用函数)> 默认绑定 特殊情况: 在某些场景下this 的绑定行为会出乎意料,你认为应当应用其他绑定规则时,实际上应用的可能是默认绑定规则更安全的编码是将 this 显式绑定到 null、undefined(等同于默认绑定 window 等)或者一个特殊对象: call 默认绑定:

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

apply 和 bind 柯里化:默认绑定

function foo (a, b) {
  return a + b
}
var fn = foo.bind(null, 3)
foo.apply(null, [4,5])
fn(4)

有些调用可能在无意中使用默认绑定规则。如果想“更安全”地忽略this 绑定,你可以使用一个DMZ 对象,比如:

 o = Object.create(null)

以保护全局对象

ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的this 绑定(无论this 绑定到什么)。这其实和ES6 之前代码中的self = this 机制一样。

function foo () {
  setTimeout(() => {
    // 这里的this 在此法上继承自foo()
    console.log(this.a)
  }, 100)
}
const obj = {
  a: 2
}
foo.call(obj) // 2

等同于:

function foo () {
  const self = this // lexical capture of this
  setTimeout(function () {
    console.log(self.a)
  }, 100)
}
const obj = {
  a: 2
}
foo.call(obj) // 2
zhangzheng-zz commented 3 years ago

第三部分 对象

属性

在对象中,属性名永远都是字符串。如果你使用 string(字面量)以外的其他值作为属性名,那它首先会被转换为一个字符串。即使是数字也不例外

var myObject = {};
myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";
myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"

可计算属性名

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

var prefix = "foo";
var myObject = {
  [prefix + "bar"]: "hello",
};
myObject["foobar"]; // hello

复制对象

经典问题深拷贝 & 浅拷贝

深拷贝: var newObj = JSON.parse( JSON.stringify( someObj ) ) 浅拷贝:Object.assign(..) 方法的第一个参数是目标对象,之后还可以跟一个或多个源对象。它会遍历一个或多个源对象的所有可枚举的自有键并把它们复制(使用= 操作符赋值)到目标对象,最后返回目标对象。

JSON.stringify 深拷贝的缺点: 1、如果obj里面有时间对象,则拷贝后时间将只是字符串的形式,而不是对象的形式; 2、如果obj里有RegExpError对象,则序列化的结果将只得到空对象; 3、如果obj里有函数,undefined,则序列化的结果会把函数undefined丢失; 4、如果obj里有NaNInfinity-Infinity,则序列化的结果会变成null 5、JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor; 6、如果对象中存在循环引用的情况也无法正确实现深拷贝。

属性描述符

ES5 开始,所有的属性都具备了属性描述符

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

使用Object.defineProperty(..)来添加一个新属性或者修改一个已有属性(如果它是configurable)并对特性进行设置。

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

不变性

var myObject = {};
Object.defineProperty(myObject, "FAVORITE_NUMBER", {
  value: 42,
  writable: false,
  configurable: false,
});
var myObject = {
  a: 2,
};
Object.preventExtensions(myObject);
myObject.b = 3;
myObject.b; // undefined

存在性

可以在不访问属性值的情况下判断对象中是否存在这个属性:

var myObject = {
  a: 2,
};
"a" in myObject; // true
"b" in myObject; // false
myObject.hasOwnProperty("a"); // true
myObject.hasOwnProperty("b"); // false

in操作符会检查属性是否在对象及其[[Prototype]]原型链中。相比之下,hasOwnProperty(..) 只会检查属性是否在myObject 对象中,不会检查[[Prototype]]链。

枚举: 在数组上应用for..in 循环有时会产生出人意料的结果,因为这种枚举不仅会包含所有数值索引,还会包含所有可枚举属性。最好只在对象上应用for..in 循环,如果要遍历数组就使用传统的for循环来遍历数值索引。

var myObject = {};
Object.defineProperty(
  myObject,
  "a",
  // 让a 像普通属性一样可以枚举
  { enumerable: true, value: 2 }
);
Object.defineProperty(
  myObject,
  "b",
  // 让b 不可枚举
  { enumerable: false, value: 3 }
);
myObject.propertyIsEnumerable("a"); // true
myObject.propertyIsEnumerable("b"); // false
Object.keys(myObject); // ["a"]
Object.getOwnPropertyNames(myObject); // ["a", "b"]

propertyIsEnumerable(..)会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足enumerable:trueObject.keys(..) 会返回一个数组,包含所有可枚举属性,Object.getOwnPropertyNames(..)会返回一个数组,包含所有属性,无论它们是否可枚举。 inhasOwnProperty(..) 的区别在于是否查找 [[Prototype]] 链,然而,Object.keys(..)Object.getOwnPropertyNames(..) 都只会查找对象直接包含的属性。

小结:

遍历

ES5 中增加了一些数组的辅助迭代器,包括forEach(..)every(..)some(..)。每种辅助迭代器都可以接受一个回调函数并把它应用到数组的每个元素上,唯一的区别就是它们对于回调函数返回值的处理方式不同。

ES6 增加了一种用来遍历数组的for..of 循环语法(如果对象本身定义了迭代器的话也可以遍历对象):

var myArray = [1, 2, 3];
for (var v of myArray) {
  console.log(v);
}
// 1
// 2
// 3
zhangzheng-zz commented 3 years ago

第四部分 混合对象“类”

混入

mixin | extend

// 非常简单的mixin(..) 例子:
function mixin(sourceObj, targetObj) {
  for (var key in sourceObj) {
    // 只会在不存在的情况下复制
    if (!(key in targetObj)) {
      targetObj[key] = sourceObj[key];
    }
  }
  return targetObj;
}
var Vehicle = {
  engines: 1,
  ignition: function () {
    console.log("Turning on my engine.");
  },
  drive: function () {
    // this 指向 Vehicle
    this.ignition();
    console.log("Steering and moving forward!");
  },
};
var Car = mixin(Vehicle, {
  wheels: 4,
  drive: function () {
    // this 绑定到 Car 上
    Vehicle.drive.call(this);
    console.log("Rolling on all " + this.wheels + " wheels!");
  },
});
// 另一种混入函数,可能有重写风险
function mixin(sourceObj, targetObj) {
  for (var key in sourceObj) {
    targetObj[key] = sourceObj[key];
  }
  return targetObj;
}
var Vehicle = {
  // ...
};
// 首先创建一个空对象并把Vehicle 的内容复制进去
var Car = mixin(Vehicle, {});
// 然后把新内容复制到Car 中
mixin(
  {
    wheels: 4,
    drive: function () {
      // ...
    },
  },
  Car
);

显式混入模式的一种变体被称为“寄生继承”

// “传统的JavaScript 类”Vehicle
function Vehicle() {
  this.engines = 1;
}

Vehicle.prototype.ignition = function () {
  console.log("Turning on my engine.");
};

Vehicle.prototype.drive = function () {
  this.ignition();
  console.log("Steering and moving forward!");
};

// “寄生类” Car
function Car() {
  // 首先,car 是一个Vehicle
  var car = new Vehicle();

  // 接着我们对car 进行定制
  car.wheels = 4;

  // 保存到Vehicle::drive() 的特殊引用
  var vehDrive = car.drive;

  // 重写Vehicle::drive()
  car.drive = function () {
    vehDrive.call(this);
    console.log("Rolling on all " + this.wheels + " wheels!");
    return car;
  };
}
var myCar = new Car();
myCar.drive();
// 发动引擎。
// 手握方向盘!
// 全速前进!

隐式混入

虽然这类技术利用了this 的重新绑定功能,但是Something.cool.call( this ) 仍然无法 变成相对(而且更灵活的)引用,所以使用时千万要小心。通常来说,尽量避免使用这样 的结构,以保证代码的整洁和可维护性。

var Something = {
  cool: function () {
    this.greeting = "Hello World";
    this.count = this.count ? this.count + 1 : 1;
  },
};
Something.cool(); 
Something.greeting; // "Hello World"
Something.count; // 1
var Another = {
  cool: function () {
    // 隐式把Something 混入Another
    Something.cool.call(this);
  },
};
Another.cool();
Another.greeting; // "Hello World"
Another.count; // 1 (count 不是共享状态)