javascript-基础篇

空闲之余,重温一下技术面试中经常提到的 JavaScript 相关概念。温故而知新,记不起来的时候回头看看多动动手指头,熟能生巧~

JS的几种数据类型

JavaScript有七种数据类型

1
2
3
4
5
6
7
Boolean
Null
Undefined
Number
String
Symbol (ECMAScript 6 新定义)
Object

其中5种为基本类型:string,number,boolean,null,undefined

Object 为引用类型(范围挺大),也包括数组、函数,
ES6出来的Symbol是原始数据类型 ,表示独一无二的值

null和undefined的差异

undefined和null的含义与用法都差不多
undefined和null在if语句中,都会被自动转 为false,相等运算符甚至直接报告两者相等。

  • null转为数字类型值为0,而undefined转为数字类型为 NaN(Not a Number)
  • undefined是代表调用一个值而该值却没有赋值,这时候默认则为undefined
  • 设置为null的变量或者对象会被内存收集器回收
  • typeof null === “object” 而 typeof undefined === ‘undefined’

作用域

  • 作用域是指代码执行时的上下文,它定义了变量以及函数生效的范围。
  • 全局作用域指的是最外层的作用域。所有在函数外部声明的变量都会
    在全局作用域当中,可以在任何地方访问到在浏览器当中,window 对象就属于全局作用域。
  • 局部作用域是指例如在某个函数内部的范围。在局部作用域中声明的变量只能够在其内部被访问到。
  • 在外部访问不到内部定义的变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var g = 'window.g'
var course = function() {
var a = 'a';
console.log(a) // -> 'a'
function course2() {
var b = 'b';
console.log(a,b) // -> 'a,b'
function course3() {
var c = 'c';
console.log(a,b,c,g) // -> 'a,b,c'
}
course3();
}
course2();
}

course()
//'a'
//'a b'
//'a b c window.g'
*从里向外逐层往上查找,直到顶层window*

变量提升 Hoisting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

es5在编译过程中,JavaScript 会自动把 varfunction 声明移动到顶部的行为被称为 hoisting.

函数声明会被完整地提升。这就意味着你在编写代码时可以在声明一个函数之前就调用它:

console.log(sum(3,6)); // 9

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

变量只会被部分提升。例如只有 var 的声明会被提升,而赋值则不会。

let 以及 const 也不会被提升。

{ /* Original code */
console.log(i); // undefined
var i = 10
console.log(i); // 10
}

{ /* 上面等同与下面代码 */
var i;
console.log(i); // undefined
i = 10
console.log(i); // 10
}

// ES6 let & const
{
console.log(i); // ReferenceError: i is not defined
const i = 10
console.log(i); // 10
}
{
console.log(i); // ReferenceError: i is not defined
let i = 10
console.log(i); // 10
}

函数表达式 与 函数声明

函数表达式 函数表达式只有被执行之后才可用

注意 它不会被提升(相当于赋值函数表达式给变量)。

1
2
3
4
var sum = function(a, b) {
return a + b;
}
sum(1,2);//必须这样调用

函数声明 则可以提升任意调用

变量声明方式:var let cost

注意:未使用 var,let 或 const 关键字声明的变量会自动变成全局变量。
var 变量没有块级作用域

闭包 Closure

1.指有权访问另一个函数作用域中的变量的函数。
2.另一个就是让这些变量的值始终保持在内存
缺点:操作不当容易造成性能问题,内存泄漏

1
2
3
4
5
6
7
8
9
 function sayHi(name){
var message = `Hi ${name}!`;
function greeting() {
console.log(message)
}
return greeting
}
var sayHiToJon = sayHi('Linyi');
sayHiToJon()// => Hi Linyi

把它改一下,猜会是什么

1
2
3
4
5
6
7
8

var message = 'Hello'
function sayHi(name){
return function () {
console.log(message,this.message)
}
var message = `Hi ${name}!`;
}

var sayHiToJon = sayHi(‘Linyi’)(); // -> ?

答案是 undefined Hello

因为 sayHi(‘Linyi’) === f(){ console.log(message,this.message) }
由于变量提升 ,所以在闭包内访问到的message 的值是 undefined

1
2
3
4
5
6
7
8
var message = `Hello !`;
function sayHi(name){
var message = undefined
return function () {
console.log(message,this.message)
}
message = `Hi ${name}!`;
}

而此时闭包的this指向window ,this.message === window.message;

所以 console.log(message,this.message) // -> undefined Hello

闭包的 Closure 的作用

1.可访问内部私有函数,创建特权方法(有权访问私有变量的公有方法叫做特权方法。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var singleton = function(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//创建对象
var object = new CustomType();
//添加特权/公有属性和方法
object.publicProperty = true;
object.publicMethod = function(){
privateVariable++;
return privateFunction();
};
//返回这个对象
return object;
}();

2.IIFE 防止污染全局变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var Module = (function() {
var _privateMethods = function () {
console.log('_privateMethods run ...')
}
var publicMethods = function () {
/* body... */
console.log('publicMethods run ...')
}
return {
publicMethods : publicMethods,
privateMethods : _privateMethods
}
})();

Module.publicMethods();

3.模仿块级作用域

1
2
3
4
5
6
7
8
9
function outputNumbers(count){
for (var i=0; i < count; i++){
//在匿名函数中定义的任何变量,都会在执行结束时被销毁
(function (j) { //传入变量 i 只能在循环中使用,使用后即被销毁。
console.log(j);
})(i);
}
}
outputNumbers(5);

上下文context (apply , call , bind)

上下文是在函数被调用时确定的,其作用改变上下文作用域this的指向

  • call() 会立即调用函数,并要求你按次序一个个传入参数。
  • apply() 也会立即调用函数,不过你需要以 数组的形式传参。
  • call() 和 .apply() 效用几乎是相同的,
    它们都可以用来调用对象中的某个方法,具体怎么使用取决于你的使用场景里如何传参更方便。
  • bind 返回的是一个新的函数而不是直接调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var name = 'wierson'; 
var obj = {
name: 'jack',//作用域值是指:变量能被访问到的范围
sayName: function() {
var name = 'linlin'
console.log(this.name)
var self = this;// -> 改变this 指向
return function() {
console.log(name, this.name, self.name)
}
}
}
obj.sayName(); // 'jack'
obj.sayName()(); // -> 'wierson jack';
// 思考:怎么让下面的this在执行环境中固定指向obj呢?
var person = {name: 'jack',age: 20}
var obj = {
age: 18,
sayName: function(sex) {
return this.name + ' is a ' + this.age + ' years old ' + sex
}
}

obj.sayName.call(person,'boy',) // -> jack is a 20 boy
obj.sayName.apply(person,['gril'])// -> jack is a 20 gril

聊聊 自定义bind函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Function.prototype.bind = function(context){
var self = this; //保存this,即调用bind方法的目标函数 如上
//与上例 console.log(self === obj.sayName ) -> true
return function() { // 返回一个新的函数
return self.apply(context,arguments)// 执行目标函数并返回结果
}
}

document.getElementById('root')
.addEventListener('click', obj.sayName.bind(person) )

*思考:面已经实现了bind方法的作用域绑定,很显然,上面的 fun_new 调用的时候并不支持传参,只能在 bind 绑定 返回之后传递参数,
如 :(obj.sayName.bind(person))(['boy']);
因为我们最终调用的是这个返回函数,
那如何让这个bind函数在绑定时候 支持传参呢:实现这样的形式bind(context,...arg)

让我们写一个兼容方法如下:
Function.prototype.Bind = function(context){
var fn = this,
args = Array.prototype.slice.call(arguments, 1);
return function(){
return fn.apply(context,
args.concat(Array.prototype.slice.apply(arguments))
)
}
}
现在兼容传参了,试下吧!
document.getElementById('root')
.addEventListener('click', obj.sayName.bind(person,'Boy'))

***注 slice 经常用来将 array-like 对象转换为 true array。
在一些框架中会经常有这种用法。

Array.prototype.slice.call(arguments);//将参数转换成真正的数组.

因为arguments不是真正的Array,虽然arguments有length属性,但是没有slice方法,

所以呢,Array.prototype.slice执行的时候,Array.prototype已经被call改成arguments了,
因为满足slice执行的条件(有length属性).
/*eg:
function list() {
return Array.prototype.slice.call(arguments,1);//2,3 单位数就是除了前面的数目
// return Array.prototype.slice.call(arguments,0,2);//1,2 双位数就是第一位数起,除了第二位数的数目
}

var list1 = list(1, 2, 3); // [1, 2, 3]*/

***

es6语法绑定

当你以 => 箭头函数的形式调用某一方法时,
相当于为其传入了当前执行上下文的 this 值。

1
2
3
4
5
6
function Dog(name) {
this.name = name;
console.log(this); // { name: 'jj' }
( () => console.log(this) )(); // { name: 'jj' }
}
var myDog = new Dog('jj')

严格模式

我们可以通过使用 “use strict” 指令来启用 JavaScript 的严格模式。它会为你的代码添加更多的限制及错误处理。

使用严格模式的好处有:
更方便调试 你能够看到更多的报错,例如在你试图为只读的全局对象或属性赋值时。

防止意外产生全局变量 对未声明的变量进行赋值时会报错

禁止无效的删除操作 尝试删除变量、函数、不可删除的属性时会报错

禁止重复的属性名及参数 如果有命名重复的属性名或者参数值就会报错

让 eval() 的调用更加安全 在 eval() 方法内部定义的变量及函数不会污染其他作用域

禁止 this 指向全局对象 当 this 的值为 null 或者 undefined时不会再默认指向到全局对象。这也就意味着在函数内部的 this 不会再默认指向 window 对象了

new 关键字

关键字 new 是一种非常特殊的调用函数的方法,被通过 new 关键字调用的函数被称为构造函数。
那么 new 到底进行了哪些操作呢?

创建了一个新的对象
新对象的原型继承自构造函数的原型
以新对象的 this 执行构造函数
返回新的对象。如果构造函数返回了一个对象,
那么这个对象会取代整个 new 出来的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

function myNew(constructor) {
var obj = {}
Object.setPrototypeOf(obj, constructor.prototype);
return constructor.apply(obj, [...arguments].slice(1)) || obj
}

那么在调用函数时使用 new 关键字与否到底有什么区别呢?

function Bird() {
this.wings = 2;
}
/
let fakeBird = Bird();
console.log(fakeBird); // undefined

let realBird= new Bird();
console.log(realBird) // { wings: 2 }

原型与继承

原型是 JavaScript 当中最容易造成困惑的一个概念。原因之一是因为原型这个词会在两个语境下使用:

原型关系
每个对象都有自己的原型对象,并会继承它原型的所有属性。

你可以通过 .proto 这种非标准的机制来获取一个对象的原型
(在ES6中,在 ES5 标准里还可以通过Object.getPrototypeOf()方法来获取)。

一般的对象还会继承一个叫做 .constructor 的属性指向 其构造函数。
当你使用构造函数生成一个对象时,
其 .proto 属性会指向构造函数的 .prototype 属性。

原型属性
每个被定义的函数都有一个名为 .prototype 的属性。

它是一个继承了原型链上所有属性的对象。
这个对象也默认包括一个 .constructor 属性,指向原始的构造函数。

所有用构造函数生成的对象也会继承一个指向这个函数的 .constructor 属性(用控制台把对象打出来会比较好理解,也可以看下面的示例)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Dog(breed, name){
this.breed = breed,
this.name = name
}

Dog.prototype.describe = function() {

console.log(`${this.name} is a ${this.breed}`)
}
const rusty = new Dog('Beagle', 'Rusty');

console.log(Dog.prototype) // { describe: ƒ , constructor: ƒ }

console.log(rusty) // { breed: "Beagle", name: "Rusty" }

console.log(rusty.describe()) // "Rusty is a Beagle"

console.log(rusty.__proto__) // { describe: ƒ , constructor: ƒ }

console.log(rusty.constructor) // ƒ Dog(breed, name) { ... }

原型链

原型链描述了对象之间相互引用的关系。

当获取一个对象的属性时,JavaScript 引擎会先从这个对象本身开始查找

如果没有找到,就会转向其原型上的属性,直到第一次找到这个属性为止

原型链上的最后一个对象是内置的 Object.prototype

而它的原型则是 null(也就是所谓原型链的终点)。

JavaScript 引擎在查找属性到这一层还是没有找到时就会返回 undefined.

自有属性与继承属性

对象的属性分为 自有 和 继承 两种。

自有属性 也就是在对象内部定义的属性。

继承属性 则是通过原型链获得的属性。

继承属性 是不可枚举的(也就是在 for/in 循环里看不到的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Car() { }
Car.prototype.wheels = 4;
Car.prototype.airbags = 1;

var myCar = new Car();
myCar.color = 'black';

/* Check for Property including Prototype Chain: */
console.log('airbags' in myCar) // true
console.log(myCar.wheels) // 4
console.log(myCar.year) // undefined

/* Check for Own Property: 检测是否是自有属性*/
console.log(myCar.hasOwnProperty('airbags')) // false — Inherited继承属性
console.log(myCar.hasOwnProperty('color')) // true
Object.create(obj) 方法可以指定原型来创建对象:

var dog = { legs: 4 };
var myDog = Object.create(dog);// 原型继承

console.log(myDog.hasOwnProperty('legs')) // false
console.log(myDog.legs) // 4
console.log(myDog.__proto__ === dog) // true

通过引用类型 继承

继承属性是原型对象上属性的一份引用拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var animal = { can: 'Run', eat: 'Bone' };
var dog = Object.create(animal);
console.log(dog.eat) // Bone


如果对象在原型上的属性被改变了,继承这一属性的对象也会受到相同的影响。

animal.eat = 'food';
console.log(dog.eat) // food

但假如属性被整个替换掉了,则不会影响到继承父的对象:

dog = { color: 'white' };
console.log(animal) // { can: 'Run', eat: 'food' };

#写在最后

限于篇幅,就先写着这么多,下篇写下谈谈JavaScript中的继承,希望对各位有帮助吧,加深理解JavaScript面向对象,巩固一下理论实战结合的思维,没准下次遇到Bug的时候,也有自己的见解不是吗