这里所说的变量作用域主要是指使用 var 定义的变量的作用域,这种变量的作用域是 function 级的,这一点在前面讲 var 语句的时候已经提过了。 JS 中的 function 是可以嵌套使用的,嵌套的 function 中的变量的作用域又是怎样的呢?请先看个例子。
1 2 3 4 5 6 7 8 9
var v = O; functionfl(){ var v = 1; functionf2(){ console.log(v); } f2(); } fl(); //1
这个例子中,定义了全局变量 v ,在函数 f1 中定义了局部变量 v ,f2定义在 f1 函数中,当调用函数 f1 时,会在其内部调用函数 f2,f2中用到了变量 v ,这时 v 会使用 f1 函数中定义的 v 。 在调用嵌套函数时,引擎会根据嵌套的层次自动创建一个参数作用域链,然后将各层次函数所定义的变量从外到内依次存放到作用域链中。例如,在上述示例中,执行函数 f2时,会首先将全局对象(浏览器中指页面本身,也就是 Window 对象)放在最下层,然后放 f1 最后放 f2。
这个例子中,首先定义了一个 function 类型的对象 F ,然后使用 F 创建了 object 类型的对象 obj ,最后在控制台打印出 obj 对象的 v 属性的值 。 使用 function (例如 F )创建 object 类型的对象(例如 obj ),只需要在 function 对象(F)前加 new 关键字就可以了。也就是说,对于一个 function 类型的对象,如果调用时前面没有 new 关键字,那么调用方法处理业务,如果前面有 new 关键字,那么用来创建对象。当然,创建对象时函数体也会被执行。 其实,经常使用的 Array,Date 等对象也都是 function 类型,可以使用 new 关键字来创建相应的 object 类型的对象实例。 为了区分主要用于处理业务的 function 和主要用于创建对象的 function ,一般会将主要用于创建对象的 function 的首字母大写而将主要用于处理业务的 function 的首字母小写 。 但这只是人为区分,实际使用时并没有什么影响。
4.2 创建过程
使用 function 创建 object 类型对象的过程可以简单地分为以下两步(可以这么理解,实际创建过程要复杂一些)。
创建 function 对应类型的空 object 类型对象 ;
将 function 的函数体作为新创建的 object 类型对象的方法来执行(主要目的是初始化 object 对象) 例如下面的例子 。
1 2 3 4 5 6
functionCar(color, displacement) { this.color= color ; this.displacement = displacement; } var car = newCar("black","2.4T"); console.log(car.color,car.displacement); //black, 2.4T
这个例子中,首先创建了 function 类型的 Car ,然后使用它新建 object 类型的 car 实例对象。在新建 car 对象时首先会新建 Car 类型的空对象 car,然后再将 Car 函数作为新建对象的方法来调用,从而初始化新建的 car 实例对象,相当于下面的过程。
1 2 3 4 5 6 7 8
functionCar(){} var car= newCar(); car.init = function(color, displacement) { this.color= color ; this.displacement = displacement; } car.init("black","2.4T"); console.log(car.color,car.displacement); //black, 2.4T
上述示例将原来 Car 对象中的函数体的内容放到新建的 car 的 init 方法中,在使用 Car 创建完 car 实例对象后,再调用 init 方法初始化,这种方式和前面例子中将初始化内容放到 Car 的函数体内的效果是完全相同的 。 需要特别注意的是,创建过程的第二步,也就是说,在使用 function 对象新建 object 对象时依然会执行 function 的函数体。通过下面的例子可以更加直观地看到这一点。
1 2 3 4 5 6 7
var name="和坤"; functionSikuquanshu() { name="纪晓岚"; } console.log(name);//和神 var skqs = newSikuquanshu(); console.log(name);//纪晓岚
这个例子中存在一个全局变量 name ,原值为“和坤”,在函数 Sikuquanshu 内部将其改为“纪晓岚”,在上述代码中并没有直接执行此函数,但在使用它创建 skqs 对象时其函数体得到了执行,这从创建 skqs 对象前后打印出的内容就能看出来,全局变量 name 被修改了。在使用 function 对象创建实例对象时一定要注意这一点 。 理解了对象创建的过程就可以理解为什么在构造函数(例如 Car )中使用 this 可以将属性添加到新创建的对象上 。 因为这时的函数体就相当于新创建的对象的一个方法,方法中的 this 指的就是新创建的对象自身,给 this 赋值就是给新创建的对象赋值,因此在 function 对象中使用 this 就可以给新创建出来的对象添加属性,就像本书第一个例子中的 Car 方法的 this.color = color;语句,这条语句会给新创建的 car 对象添加 color 属性并将 color 参数的值赋给它。对于这一点,在后面讲到 object 的属性时还会做进一步介绍。 05 点运算符与 this 关键字 js对象在内存中怎样保存
这个例子中,在创建完 car 对象后又给它添加了 logColor 方法,可以打印出 car 的 color 属性 。 添加完 logColor 方法后直接调用就可以打印出 car 原来的 color 属性值( black ) 。 然后, 将其修改为 red ,再打印就打印出了 red 。 最后,使用 delete 删除 car 的 color 属性,这时再调用 logColor 方法就会打印出 undefined 。
5.4 三种子类型的关系
function 的三种子类型是相互独立的,它们只能在自己所对应的环境中使用而不能相互调用,例如下面的例子 。
ES 中对象的属性其实有三种类型,前面使用的属性只是其中的一种 。 属性的三种类型分别为:命名数据属性( named data properties )、命名访问器属性( named accessor properties) 和内部属性( internal properties ) 。下面我们分别学习 。
var person = {name :"张三"} person.name = "李四"//修改原有属性的值 person.age = 88; //添加新属性
这个例子中,首先使用花括号定义了 person 对象,其中包含 name 属性,当给它的 name 属性赋予新值时会改变其 name 属性的值,而当给 age 属性赋值时,由于 person 原来没有 age 属性,所以会先添加 age 属性,然后将其值设置为 88 。 在 function 中使用 this 创建属性其实也是这种添加属性方式的一种特殊用法。因为在 function 创建 object 类型对象时,其中的 this 就代表创建出来的对象,而且刚创建出来的对象是没有自定义的命名属性的,所以使用 this 和点操作符就可以将属性添加到创建的对象中,例如下面的例子。
1 2 3 4 5
functionPerson() { this.name = "孙悟空"; } var person = newPerson() console.log(person.name); //孙悟空
在这个例子中,首先定义了 function 类型的 Person ,然后用其创建了 person 对象,创建完成后会自动调用 Person 方法体中的 this.name = "孙悟空"语句,这时,由于 this 所代表的 person 对象并没有 name 属性,所以会自动给它添加 name 属性,这也就是创建的 person 对象具有 name 属性的原因了。
这个例子使用 Object 的 defineProperties 方法给 obj 对象添加了 name 和 color 两个属性 。 在这个例子中, 因为 name 属性的 writable 为 false ,所以 obj 的 name 属性是不可以修改的 。 当我们将其值修改为 peter 后, 打印出的还是原来的 lucy ,这说明修改并没有作用 。 而且, 使用 defineProperties 方 法添加属性时 writable 的默认值就是 false 。
通过 prototype 属性创建
使用 function 创建的 object 实例对象可以使用 function 对象的 prototype 属性对象中的属性,这一点我们在前面已经多次证实过。严格来说, function 对象的 prototype 中的属性并不会添加到创建的实例对象中,但创建的对象可以调用,这样就相当于可以将 prototype 中的属性添加到创建的对象中。因此,如果给 function 的 prototype 添加了属性,那么也就相当于给创建的对象添加了属性,而且在对象创建完成之后还可以再添加,例如下面的例子。
1 2 3 4
functionShop(){} var shop = newShop(); Shop.prototype.type = "网络销售" console.log(shop.type); //网络销售
在这个例子中,使用 Object 的 defineProperty 方法给 person 对象添加了 name 访问器属性,其值保存在 name 命名数据属性中,当我们获取 name 的值或者给 name 设置新值的时候就会调用相应的 getter 和 setter 方法。我们可以使用 Object 的 getOwnPropertyDescriptor 方法来获取 name 属性的所有特性。另外,我们也可以在 function 中使用 Object 的 defineProperty 方法给其创建的对象实例添加属性,这时只要将对象写为 this 即可,而且这种方式还可以使用 function 的内部变量。例如,我们将上个例子中的 person 对象改为由 function 类型的 Person 来创建。
functionlog(msg) { console.log(msg); } functionPerson() { var name = "peter"; Object.defineProperty(this, "name", { get: function() { log("getting name"); return name; }, set: function(newNarne) { log("name is changed to " + newNarne); name = newNarne } }) } var person = newPerson(); log(Object.getOwnPropertyDescriptor(person, "name"));//{enumerable: false, configurable: false, get: ƒ, set: ƒ} person.name = "lucy"; //name is changed to lucy log(person.name);//getting name, lucy
这个例子就在 function 中使用 defineProperty 方法创建了名为 name 的访问器属性,并在其中定义了getter 和 setter ,即Get 和 Set 特性 。 在这个例子中,我们将它的值保存到 Person 的局部变量 name 中,这样就可以屏蔽通过实例对象直接调用访问器属性的值 。
直接量是指不需要创建对象就可以直接使用的变量 。 ES 中的直接量主要有三种类型: 表示字符串的 string 类型 、 表示数字的 number 类型和表示 true/false 的 boolean 类型 。 对于直接量,在使用时直接将值赋给变量就可以了,例如下面的代码 。
1 2 3 4 5 6 7 8 9 10
var str ="hello word"; console.log(typeof str}; var num = 210; console.log(typeof num}; var numl = 325 . 7; console.log(typeof numl); var flag = false; console.log(typeof flag}; flag = 376; console.log(typeof flag);
当我们直接将值赋给变量后, ES 就会自动判断其类型,而且当参数值发生变化后(例如此例中的 flag ),其类型也会自动跟着发生变化,即 ES 是一种弱类型的语言。另外,对于数字类型来说,无论是整数还是小数都是 number 类型。
var m = 5; var n = m; m = 7; console.log(n,m); //5 ,7
这个例子中,虽然将 m 赋值给 n ,但只是将 m 的值赋给 n ,当 m 发生变化时, n 并没有发生变化,这一点和对象类型是不同的。如果是对象类型,那么赋值的时候是将对象的地址赋给新值,当对象中的属性发生变化时两个对象都会发生变化,例如下面的例子。
1 2 3 4
var obj = {m:5}; var newObj = obj; obj.m = 7; console.log(newObj.m) // 7
在这个例子中, obj 和 newObj 使用的是同一个对象,当 obj 中的 m 属性发生变化时, newObj 中的 m 属性也会发生变化。
4.2 直接量的封包与解包
直接量是单个值,并不是对象,当然也就没有属性。但是在 ES 中,我们可以使用直接量来调用属性方法,例如下面的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
functionlog(msg) { console.log(msg); } var s = "hello"; log(s.toUpperCase()); //HELLO log(s.substr(3, s.length)); //lo var n = 325.764; log(n.toPrecision(5)); //325.76 log(n.toExponential(5)); //3.25764e+2 n = 7596389; log(n.toLocaleString()); //7,596,389 log(n.toLocaleString("zh-Hans-CN-u-nu-hanidec")); //七,五九六,三八九 var b = true; log(b === "true"); //false log(b.toString() === "true"); //true log(b.toString() === true); //false
这个例子中,由 String 类型的变量 s 调用了 toUpperCase 、 substr 和 length 属性,分别用于将 s 的值变为大写、截取 s 的一部分及获取 s 的长度; number 类型的变量 n 调用了 toPrecision , toExponential 和 toLocaleString 方法,分别用于设置 n 的精度、将 n 转换为科学计数法,以及将 n 转换为本地数组表达格式; boolean 类型的 b 属性调用了 toString 方法,用于将 boolean 转换为 string 类型 。 既然直接量只是一个值而不是对象,那么它怎么可以调用属性方法呢?原来自有一种叫作自动封包/解包的功能 。 封包/解包对于熟悉 Java 的读者来说一定不会陌生(在 Java 中也称装箱/拆箱,它们的含义都一样),其作用是在程序执行过程中按照实际需要自动在直接量和其所对应的对象类型之间进行转化 。 将直接量转换为对应的对象进行处理叫作封包,反过来,将对象转换为直接量叫作解包 。 封包和解包都是 JS 引擎自动完成的,而且只是为了完成程序的执行而进行的暂时转换,并不会实际修改变量的类型。有了封包/解包我们就不需要考虑什么时候使用直接量什么时候使用对象了,而且也不需要担心变量类型会发生变化 。上面的例子就使用了封包功能,下面我们再来看一个使用到解包功能的例子 。
1 2 3 4 5 6 7
var m = newNumber(5); var n = m + 2;//m会自动解包为直接量后再计算 console.log(n); //7 console.log(typeof m);//object console.log(typeof n);//number console.log(m instanceofNumber);//true console.log(n instanceofNumber);//false
这个例子中,定义了对象类型的 m 变量,当对其进行加法计算时 m 会自动解包为直接量再进行计算,但是计算之后 m 的类型并不会变化,还是 object 类型。实际使用中我们很少直接使用直接量所对应的包装对象,所以封包功能使用得非常多,但是解包功能相对使用得就比较少了。 js类型转换
4.3 直接量的包装对象
直接量所对应的对象叫作包装对象, string 、 number 、 boolean 所对应的包装对象分别是 String 对象、 Number 对象和 Boolean 对象,它们都是 function 类型的对象。本节我们就来学习这三个对象。一个对象最重要的就是它所包含的属性,而 function 对象的属性又分为两大类,一类是它自身的属性,另一类是它创建的 object 类型实例对象的属性,创建的实例对象的属性又分为实例自己的属性和 function 的 prototype 的属性。学习 function 类型对象最重要的是学习两个方面的内容: function 作为函数的功能和它对应的属性。对于包装类型的对象来说,作为函数使用时的功能都是将传入的参数转换为 function 所对应的直接量,例如,使用如 String("abc") 可以新建值为 abc 的字符串类型的直接量等,其实和不使用函数的效果是一样的,所以学习包装类型对象主要是学习它所对应的属性。包装对象的属性和普通对象的属性没有什么区别,也是一共包括三部分: function 对象自身拥有的属性、创建的实例对象所拥有的属性和 function 的 prototype 属性对象中的属性。下面我们就从这三个方面分别学习这三个包装对象。
classPerson { // 构造函数,用于初始化类的实例 constructor(name, age) { this.name = name; this.age = age; } // 实例方法 greet() { console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`); } // 静态方法 staticsayHello() { console.log('Hello from Person class'); } }
创建实例
使用 new 关键字创建类的实例。
1 2 3 4
const person1 = newPerson('Kimi', 25); person1.greet(); // Hello, my name is Kimi and I am 25 years old. const person2 = newPerson('Moonshot', 30); person2.greet(); // Hello, my name is Moonshot and I am 30 years old.
调用静态方法
静态方法可以通过类名直接调用,不需要创建实例。
1
Person.sayHello(); // Hello from Person class
类的继承
ES6 的类支持继承,可以使用 extends 关键字继承一个父类,并使用 super 关键字调用父类的构造函数和方法。
classPerson { constructor(name, age) { this.name = name; this.age = age; } greet() { console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`); } staticsayHello() { console.log('Hello from Person class'); } } classEmployeeextendsPerson { constructor(name, age, jobTitle) { super(name, age); // 调用父类的构造函数 this.jobTitle = jobTitle; } greet() { super.greet(); // 调用父类的 greet 方法 console.log(`I am a ${this.jobTitle}.`); } staticsayHello() { console.log('Hello from Employee class'); } } const employee = newEmployee('Kimi', 25, 'Software Engineer'); employee.greet(); // Hello, my name is Kimi and I am 25 years old. I am a Software Engineer. Employee.sayHello(); // Hello from Employee class
通过使用 class 语法,JavaScript 的面向对象编程变得更加简洁和直观,同时也提供了更强大的功能,如继承和静态方法等。
// ES6 类 classPerson { constructor(name, age) { this.name = name; this.age = age; } greet() { console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`); } } const person = newPerson('Kimi', 25); person.greet(); // Hello, my name is Kimi and I am 25 years old.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// TypeScript 类 classPerson { name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } greet(): void { console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`); } } const person = newPerson('Kimi', 25); person.greet(); // Hello, my name is Kimi and I am 25 years old.
2. 访问修饰符
ES6:ES6 的类不支持访问修饰符(如 public、private、protected)。
TypeScript:TypeScript 支持访问修饰符,可以明确指定类的属性和方法的访问权限。
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
classPerson { publicname: string; privateage: number; constructor(name: string, age: number) { this.name = name; this.age = age; } publicgreet(): void { console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`); } privategetAge(): number { returnthis.age; } } const person = newPerson('Kimi', 25); person.greet(); // Hello, my name is Kimi and I am 25 years old. // person.getAge(); // 抛出错误:Property 'getAge' is private and only accessible within class 'Person'.
// ES6 类 classPerson { constructor(name, age) { this.name = name; this.age = age; } greet() { console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`); } } const person = newPerson('Kimi', '25'); // 运行时不会报错,但可能会导致逻辑错误 person.greet(); // Hello, my name is Kimi and I am 25 years old.
1 2 3 4 5 6 7 8 9 10 11 12 13
// TypeScript 类 classPerson { name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } greet(): void { console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`); } } const person = newPerson('Kimi', '25'); // 编译时报错:Argument of type 'string' is not assignable to parameter of type 'number'.
classPerson { constructor(publicname: string, privateage: number) { } greet(): void { console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`); } } const person = newPerson('Kimi', 25); person.greet(); // Hello, my name is Kimi and I am 25 years old.
描述:向浏览器的文档流中直接写内容,可以用于动态生成 HTML 内容。不过在现代开发中,推荐使用更现代的方法(如 innerHTML、textContent 等)来操作 DOM,因为 document.write 有一些已知的问题和限制。
示例:
1
document.write('<div>Hello, World!</div>');
5.2 document.getElementById(id).innerHTML
描述:获取或设置元素的 HTML 内容。
示例:
1 2 3
let element = document.getElementById('myElement'); console.log(element.innerHTML); // 获取元素的 HTML 内容 element.innerHTML = '<span>New Content</span>'; // 设置元素的 HTML 内容
5.3 document.getElementById(id).textContent
描述:获取或设置元素的文本内容,与 innerHTML 不同,textContent 不会解析 HTML 标签。
示例:
1 2 3
let element = document.getElementById('myElement'); console.log(element.textContent); // 获取元素的文本内容 element.textContent = 'New Text Content'; // 设置元素的文本内容