javascript-js高阶解析 Jan 8, 2023 · ecmascript javascript · 分享到: js高阶解析 关于JavaScript的一些高阶问题,建议在入门之后当作Q&A查看。 语法形式 变量与作用域 变量提升 作用域的覆盖 null与undefined ==与=== for ... of 和 for ... in 遍历 类中的方法 this的指向问题 箭头函数 面向对象编程 构造函数 类中属性的类型 原型对象prototype 语法形式 JavaScript的语法与C/C++,Java类似,通过;、{...}来分割语句,区分大小写,而非像Python一样通过缩进。Javascript解析器对缩进没有讲究。但是,JavaScript并不强制要求在每个语句的结尾加;,浏览器中负责执行JavaScript代码的引擎会自动在每个语句的结尾补上;,但是自动加分号在某些情况下会改变程序的语义,导致运行结果与期望不一致。因此在实际编程中,强烈建议像C/C++,Java一样严谨使用等号。例子如下: 1var x = 1; 2function test1(par){ 3 if(x === 1) 4 return true 5} 6console.log(test1(x)); 7 8function test2(par){ 9 if(x === 1) 10 return 11 true 12} 13console.log(test2(x)); 两个函数test1(par),test2(par)的区别就是return语句是否分成了两行写。但是执行结果却不一样: 1test1: true 2test2: undefined 原因就在在引擎执行时,在test2函数的return后面自动加了;,在执行时就成了: 1function test2(par){ 2 if(x === 1) 3 return ; //自动加了分号,函数到此处直接返回 4 true; 5} 因此test2的返回结果就是undefined。 变量与作用域 JavaScript提供了三类变量声明方法。 直接赋值使用,x=100; var声明,var x = 100; let, const声明,let x = 100; const y = 3.14; 它们三者各有区别。 首先,当我们使用方式1,直接给未声明变量赋值时,所产生的变量都是全局变量!无论是在函数体内,还是函数体外,只要使用这种声明方式的都是全局变量。 当我们使用方式2中var声明时,在函数外部声明就是全局变量;在函数内部声明就是局部变量。var变量可以重新声明和修改。需要注意的是,var的局部变量范围是整个函数体,不像C/C++,Java那样作用域是代码块。 为了实现更细代码块级别的作用域划分,在ES6中引入了let和const。块是由{}界定的代码块,匹配的大括号中就是一个块,代码块可以嵌套定义。大括号内的任何内容都包含在一个块级作用域中,而let和const声明的变量都只在对应的代码块中有效。如果在全局中使用let那么定义的就是全局变量。例子如下: 1let times = 4; 2 3if (times > 3) { 4 let hello = 'say Hello'; 5 console.log(hello); // "say Hello" 6} 7console.log(hello); // hello is not defined 我们看到在其代码块(定义它的花括号)之外使用hello会返回错误。这是因为let变量是块范围的。 就像var一样,用let声明的变量可以在其范围内被修改。但与var不同的是,let变量无法在其作用域内被重新声明。 来看下面的例子 1let greeting = 'say Hi'; 2greeting = 'say Hello instead';//正常执行 这是重新给greeting变量赋值,修改时允许的。 1let greeting = 'say Hi'; 2let greeting = 'say Hello instead'; // SyntaxError: Identifier 'greeting' has already been declared 两个let相当于重新声明greeting,会报错:变量已经被声明。但是,如果在不同的作用域中定义了相同的变量,则不会有错误,这属于作用域的覆盖。这个事实说明:使用let,是比var更好的选择。当使用let时,你不必费心思考变量的名称,因为变量仅在其块级作用域内存在。现在推荐使用let来声明变量。 const声明的变量保持常量值,和let一样也是在对应代码块内有效。const不能被修改并且不能被重新声明,因此每个const声明都必须在声明时进行初始化。虽然const声明的变量不可以修改,但是可以修改const对象的属性,比如: 1const greeting = { 2 message: 'say Hi', 3 times: 4, 4};//声明const对象 5 6const greeting = { 7 words: 'Hello', 8 number: 'five', 9}; // error: Assignment to constant variable. 10 11greeting.message = 'say Hello instead';//可以修改其属性 变量提升 变量提升是JavaScript的一种机制:在执行代码之前,变量和函数声明会移至其作用域的顶部。注意,仅仅是声明,赋值操作并不会提升。这意味着如果我们这样做: 1console.log(greeter); 2var greeter = 'say hello'; 生面的代码会被解释为: 1var greeter; 2console.log(greeter); // greeter is undefined 3greeter = 'say hello'; var,let,const都会被提升,区别是var提升到顶部后使用undefined值对其进行初始化。用let声明的变量被提升到作用域的顶部时不会对值进行初始化,因此,如果你尝试在声明前使用let变量,则会收到Reference Error。const声明也会被提升到顶部,但是没有初始化,最好将const声明都放到代码顶部。 作用域的覆盖 当全局变量跟局部变量重名时,局部变量的作用域会覆盖掉全局变量的作用域,当离开局部变量的作用域后,又重回到全局变量的作用域。如果代码块内的局部变量与外部局部变量重名,代码块内局部变量作用域优先级最高。 1var str = "我是全局变量"; 2 3function fn() { 4 var str = "我是局部变量"; 5 console.log(str); //结果:我是局部变量 6 { 7 let str = "我是块内局部变量"; 8 console.log(str); //结果:我是块内局部变量 9 } 10 console.log(str); //结果:我是局部变量 11} 12fn(); 13console.log(str);//结果:我是全局变 运行结果为: 1我是局部变量 2我是块内局部变量 3我是局部变量 4我是全局变量 null与undefined 目前,null和undefined基本是同义的,只有一些细微的差别。 null表示没有对象,即该处不应该有值。典型用法是: 作为函数的参数,表示该函数的参数不是对象。 作为对象原型链的终点。 1Object.getPrototypeOf(Object.prototype) 2// null undefined表示缺少值,就是此处应该有一个值,但是还没有定义。典型用法是: 变量被声明了,但没有赋值时,就等于undefined。 调用函数时,应该提供的参数没有提供,该参数等于undefined。 对象没有赋值的属性,该属性的值为undefined。 函数没有返回值时,默认返回undefined。 1var i; 2i // undefined 3 4function f(x){console.log(x)} 5f() // undefined 6 7var o = new Object(); 8o.p // undefined 9 10var x = f(); 11x // undefined ==与=== JavaScript在设计时,有两种比较运算符: 第一种是==比较,它会自动转换数据类型再比较,很多时候,会得到非常诡异的结果; 第二种是===比较,它不会自动转换数据类型,如果数据类型不一致,返回false,如果一致,再比较。 由于JavaScript这个设计缺陷,不要使用==比较,始终坚持使用===比较。 另一个例外是NaN这个特殊的Number与所有其他值都不相等,包括它自己: 1NaN === NaN; // false 唯一能判断NaN的方法是通过isNaN()函数: 1isNaN(NaN); // true 注意浮点数的相等比较: 11 / 3 === (1 - 2 / 3); // false 这不是JavaScript的设计缺陷。浮点数在运算过程中会产生误差,因为计算机无法精确表示无限循环小数。要比较两个浮点数是否相等,只能计算它们之差的绝对值,看是否小于某个阈值: 1Math.abs(1 / 3 - (1 - 2 / 3)) < 0.0000001; // true` 对于Array,Object等高级类型,==和===是没有区别的进行 "指针地址" 比较。 for ... of 和 for ... in 遍历 for ... in是ES5标准引入的语法,用于遍历键值对对象(可遍历对象,数组,字符串,map等),输出的是键(key)。此外,其不仅可以遍历数字键名,还会遍历原型(prototype)和用户手动添加的其他键。 for ... of是ES6标准引入的语法,用于拥有迭代器对象的集合遍历(可遍历对象,数组,字符串,map,set,arguments对象,普通对象没有迭代器无法遍历)输出的是值(value)。 此外,还可以使用可迭代对象内置的forEach方法(ES5.1标准引入),它接收一个函数,每次迭代就自动回调该函数。 类中的方法 在一个对象中绑定函数,称为这个对象的方法。绑定到对象上的函数称为方法,和普通函数也没啥区别。例子如下: 1var xiaoming = { 2 name: '小明', 3 birth: 1990, 4 age: function () { 5 var y = new Date().getFullYear(); 6 return y - this.birth; 7 } 8}; 此时,age()就是对象xiaoming的方法,使用时直接调用即可xiaoming.age()。 它在内部使用了一个this关键字,这个东东是什么?在一个方法内部,this是一个特殊变量,它始终指向当前对象,也就是xiaoming这个变量。所以,this.birth可以拿到xiaoming的birth属性。 this的指向问题 总结:一般情况下,this指向生成实例时的上一级对象。难点就是判断何时生成对象实例。 this永远指向一个对象; this的指向完全取决于函数调用时的位置,而非声明时的位置; 箭头函数 ES6标准新增了一种新的函数:Arrow Function(箭头函数)。 像Lambda表达式,是一种语法糖,简化匿名函数。 1x => x * x 上面的箭头函数相当于: 1function (x) { 2 return x * x; 3} 箭头函数有两种格式,一种像上面的,只包含一个表达式,连大括号{ ... }和return都省略掉了。还有一种可以包含多条语句,这时候就不能省略大括号{ ... }和return。 面向对象编程 JavaScript的面向对象编程和大多数其他语言如Java、C#的面向对象编程都不太一样。如果你熟悉Java或C#,很好,你一定明白面向对象的两个基本概念:类(对象的类型模板)和实例(根据类创建的对象)。不过,在JavaScript中,这个概念需要改一改。JavaScript不区分类和实例的概念,而是通过原型(prototype)来实现面向对象编程。 1class1.__proto__=class2//低版本浏览器可能不适用 表示class1通过class2来生成新的对象。对于低版本浏览器不适用的场景,建议使用Object.create()方法可以传入一个原型对象,并创建一个基于该原型的新对象,但是新对象什么属性都没有。 1var class1 = Object.create(class2)//默认class1的所有值为空 JavaScript的原型链和Java的Class区别就在,它没有“Class”的概念,所有对象都是实例,所谓继承关系不过是把一个对象的原型指向另一个对象而已。 构造函数 new一个函数,就可以把这个函数当成构造函数使用。 类中属性的类型 由于Javascript在设计之处并不是面向对象的语言,因此在类设计方面没有现在常见的特性例如访问级别修饰符(public,private,protected),读方法(getter)和写方法(setter),属性的枚举等等。尤其在Java语言中出现的Javabean规范被证明对减少面向对象编程中的BUG具有积极意义,这促使其他编程语言包括Javascript也想方设法实现类似的功能,其中属性类型就是此方面的实践。为了表示方便,标准中一般使用[[Prooerty]]来表示类中内容的属性,本文中依照标准的表示方法使用。 JavaScript属性类型分为两种,数据属性和访问器属性。最开始Javascript只有数据属性,基本上一般教程里看到的类属性都是数据属性;在ES5标准中,为了增加数据的封装性和可控性,又增加了访问器属性(Accessor)。访问器属性更类似于面向对象编程中成员属性的get(),set()函数,在这些函数中,我们能够对数据的读写进行一定控制。 我们先看数据属性,其包含四个特性: [[Configurable]]:可配置性。表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是true。 例外,当[[Configurable]]为false时,若[[Writable]]为true,我们可以修改[[value]]的值或将[[Writable]]改为false。 [[Enumerable]]:可遍历性。表示属性是否可以通过for-in循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是true。 由于Javascript中Array也是对象,因此我们给Array自定义的属性也会在for-in循环中带出来。目前建议数组Array的数据遍历用for-of循环。 [[Writable]]:是否可写。表示属性的值是否可以被修改。默认情况下,所有直接定义在对象上的属性的这个特性都是true。 [[Value]]:包含属性实际的值。未初始化的默认值为 undefined。 在上面四个数据属性中,最容易理解的是[[value]],我们用各种方法初始化对象就是设置属性的[[value]]。 1let person1 = {name: "Alice"}; // 数据属性name的[[value]]是Alice 2let person2 = {}; 3person2.name = "Bob"; // 数据属性name的[[value]]是Bob 之后对这个值的任何修改都会保存在[[value]]这个特性。相对于 [[value]]可以方便地修改,剩下三个数据属性的特性并不能直接修改,就必须使用Object.defineProperty()方法。这个方法接收 3 个参数:要给其添加属性的对象、属性的名称和一个特性描述符对象。最后一个参数,即描述符对象上的属性可以包含:configurable、enumerable、writable和value,跟相关特性的名称一一对应。根据要修改的特性,可以设置其中一个或多个值。 需要指出,区别于特性初始化默认为true的情形,在调用Object.defineProperty()时,configurable、enumerable和writable的值如果不指定,则都默认为false。多数情况下,可能都不需要Object.defineProperty()提供的这些强大的设置,但要理解 JavaScript对象,就要理解这些概念。 例子: 1let person = {}; 2 3//数值上等同于 person.name = "Bill";person.income=10000;person.tax=2000;person.gender="male"。但是属性特性不同 4Object.defineProperty(person,"name", {configurable:true, enumerable:true,writable:true,value: "Bill"}); 5Object.defineProperty(person,"income", {configurable:true, enumerable:true,writable:true,value: 10000}); 6Object.defineProperty(person,"tax", {configurable:true, enumerable:false,writable:true,value: 2000}); 7Object.defineProperty(person,'gender',{enumerable:true,value: 'male'}); 8//for-in 遍历 9for(let par in person) { 10 console.log(par+":"+person[par]); 11} 12console.log('Tax: '+person.tax) 13 14//尝试修改gender属性与income属性 15console.log('尝试修改gender属性与income属性'); 16person.income = 20000; 17console.log('Income: '+person.income); 18person.gender = 'female'; 19console.log('Gender: '+person.gender); 显示结果: 1name:Bill 2income:10000 3gender:male 4Tax: 2000 5尝试修改gender属性与income属性 6Income: 20000 7Gender: male 由于没有将person.tax的可遍历[[enumerable]]设置为true,因此js采用默认值false,在使用for-in遍历时,不会显示person.tax。不过我们依旧可以使用person.tax来访问。由于person.income的[[writable]]是true,我们可以顺利地修改其值。然而person.gender的[[writable]]是false,这个属性的值就不能再修改了,在非严格模式下尝试给这个属性重新赋值会被忽略。在严格模式下,尝试修改只读属性的值会抛出错误。 原型对象prototype 在ES6中新增了面向对象编程的模式,支持了Class,extends等面向对象关键字。不过在ES6之前,JavaScript也是能够实现面向对象编程的,用的就是原型对象protptype,新增的功能不过是对既有功能的封装,让其更符合现代编程模式。 javascript中的prototype更像是面向对象设计中的类,prototype属性指向的是原型类,构造函数的原型prototype是这个prototype对象,原型对象的constructor指向构造函数。所以说prototype扮演了js中类class的角色,对象与类直接有关系,构造函数也和类直接有关系,实例对象与构造函数通过类间接联系在一起。 构造函数通过prototype属性指向原型对象,实例通过__proto__属性指向原型对象。关系如下图: 上图展示了Person构造函数、Person的原型对象和Person现有两个实例之间的关系。注意,Person.prototype指向原型对象,而Person.prototype.contructor指回Person构造函数。原型对象包含constructor属性和其他后来添加的属性。Person的两个实例person1和person2都只有一个内部属性指回Person.prototype,而且两者都与构造函数没有直接联系。另外要注意,虽然这两个实例都没有属性和方法,但person1.sayName()、person2.sayName()可以正常调用。这是由于对象属性查找机制的原因。